import zlib from 'pako' ; import { ABIDef , ABISerializable , AnyAction , AnyTransaction , API , APIClient , Bytes , Name , NameType , PermissionLevel , PermissionLevelType , PrivateKey , Serializer , Signature , SignedTransaction , Transaction , } from '@greymass/eosio' ; import { AbiProvider , CallbackPayload , ChainId , ChainIdType , IdentityProof , PlaceholderName , PlaceholderPermission , ResolvedSigningRequest , ResolvedTransaction , SigningRequest , SigningRequestCreateArguments , } from 'eosio-signing-request' ; import { CancelError , IdentityError } from './errors' ; import { LinkChainConfig , LinkOptions } from './link-options' ; import { LinkChannelSession , LinkFallbackSession , LinkSession } from './link-session' ; import { LinkStorage } from './link-storage' ; import { LinkTransport } from './link-transport' ; import { LinkCreate } from './link-types' ; import { BuoyCallbackService , LinkCallback , LinkCallbackService } from './link-callback' ; import { sessionMetadata } from './utils' ; /**
 * Payload accepted by the [[Link.transact]] method.
 * Note that one of `action`, `actions` or `transaction` must be set.
 */ export interface TransactArgs { /** Full transaction to sign. */ transaction ? : AnyTransaction ; /** Action to sign. */ action ? : AnyAction ; /** Actions to sign. */ actions ? : AnyAction [ ] ; } /**
 * Options for the [[Link.transact]] method.
 */ export interface TransactOptions { /**
     * Whether to broadcast the transaction or just return the signature.
     * Defaults to true.
     */ broadcast ? : boolean ; /**
     * Chain to use when configured with multiple chains.
     */ chain ? : LinkChainType ; /**
     * Whether the signer can make modifications to the request
     * (e.g. applying a cosigner action to pay for resources).
     *
     * Defaults to false if [[broadcast]] is true or unspecified; otherwise true.
     */ noModify ? : boolean ; } /**
 * The result of a [[Link.transact]] call.
 */ export interface TransactResult { /** The resolved signing request. */ resolved : ResolvedSigningRequest ; /** The chain that was used. */ chain : LinkChain ; /** The transaction signatures. */ signatures : Signature [ ] ; /** The callback payload. */ payload : CallbackPayload ; /** The signer authority. */ signer : PermissionLevel ; /** The resulting transaction. */ transaction : Transaction ; /** Resolved version of transaction, with the action data decoded. */ resolvedTransaction : ResolvedTransaction ; /** Push transaction response from api node, only present if transaction was broadcast. */ processed ? : { [ key ] : any ; } ; } /**
 * The result of a [[Link.identify]] call.
 */ export interface IdentifyResult extends TransactResult { /** The identified account, not present unless [[LinkOptions.verifyProofs]] is set to true. */ account ? : API . v1 . AccountObject ; /** The identity proof. */ proof : IdentityProof ; } /**
 * The result of a [[Link.login]] call.
 */ export interface LoginResult extends IdentifyResult { /** The session created by the login. */ session : LinkSession ; } internal export type LinkChainType = LinkChain | ChainIdType | number ; /**
 * Class representing a EOSIO chain.
 */ export class LinkChain implements AbiProvider { /** EOSIO ChainID for which requests are valid. */ public chainId : ChainId ; /** API client instance used to communicate with the chain. */ public client : APIClient ; private abiCache = new Map < string , ABIDef > ( ) ; private pendingAbis = new Map < string , Promise < API . v1 . GetAbiResponse > > ( ) ; internal constructor ( chainId , clientOrUrl ) { this . chainId = ChainId . from ( chainId ) ; this . client = typeof clientOrUrl === 'string' ? new APIClient ( { url : clientOrUrl } ) : clientOrUrl ; } internal public async getAbi ( account ) { const key = String ( account ) ; let rv = this . abiCache . get ( key ) ; if ( ! rv ) { let getAbi = this . pendingAbis . get ( key ) ; if ( ! getAbi ) { getAbi = this . client . v1 . chain . get_abi ( account ) ; this . pendingAbis . set ( key , getAbi ) ; } rv = ( await getAbi ) . abi ; this . pendingAbis . delete ( key ) ; if ( rv ) { this . abiCache . set ( key , rv ) ; } } return rv as ABIDef ; } } example export class Link { /** Package version. */ static version : string = '__ver' ; /** Chains this instance is configured with. */ public readonly chains : LinkChain [ ] ; /** Transport used to deliver requests to the users wallet. */ public readonly transport : LinkTransport ; /** Storage adapter used to persist sessions. */ public readonly storage ? : LinkStorage ; private callbackService : LinkCallbackService ; private verifyProofs : boolean ; private encodeChainIds : boolean ; /** Create a new link instance. */ constructor ( options ) { if ( typeof options !== 'object' ) { throw new TypeError ( 'Missing options object' ) ; } if ( ! options . transport ) { throw new TypeError ( 'options.transport is required' ) ; } let chains = options . chains || [ ] ; if ( options . chainId && options . client ) { chains = [ { chainId : options . chainId , nodeUrl : options . client } ] ; } if ( chains . length === 0 ) { throw new TypeError ( 'options.chains is required' ) ; } this . chains = chains . map ( ( { chainId , nodeUrl } ) => { if ( ! chainId ) { throw new Error ( 'options.chains[].chainId is required' ) ; } if ( ! nodeUrl ) { throw new Error ( 'options.chains[].nodeUrl is required' ) ; } return new LinkChain ( chainId , nodeUrl ) ; } ) ; if ( options . service === undefined || typeof options . service === 'string' ) { this . callbackService = new BuoyCallbackService ( options . service || LinkOptions . defaults . service ) ; } else { this . callbackService = options . service ; } this . transport = options . transport ; if ( options . storage !== null ) { this . storage = options . storage || this . transport . storage ; } this . verifyProofs = options . verifyProofs !== undefined ? options . verifyProofs : LinkOptions . defaults . verifyProofs ; this . encodeChainIds = options . encodeChainIds !== undefined ? options . encodeChainIds : LinkOptions . defaults . encodeChainIds ; } note public get client ( ) { return this . chains [ 0 ] . client ; } throws internal public getChain ( chain ) { if ( chain instanceof LinkChain ) { return chain ; } if ( typeof chain === 'number' ) { const rv = this . chains [ chain ] ; if ( ! rv ) { throw new Error ( `Invalid chain index: ${ chain }` ) ; } return rv ; } const id = ChainId . from ( chain ) ; const rv = this . chains . find ( ( c ) => c . chainId . equals ( id ) ) ; if ( ! rv ) { throw new Error ( `Unsupported chain: ${ id }` ) ; } return rv ; } internal public async createRequest ( args , chain ? , transport ? ) { const t = transport || this . transport ; let request ; if ( chain || this . chains . length === 1 ) { const c = chain || this . chains [ 0 ] ; request = await SigningRequest . create ( { ... args , chainId : c . chainId , broadcast : false , } , { abiProvider : c , zlib } ) ; } else { request = await SigningRequest . create ( { ... args , chainId : null , chainIds : this . encodeChainIds ? this . chains . map ( ( c ) => c . chainId ) : undefined , broadcast : false , } , { abiProvider : this . chains [ 0 ] , zlib } ) ; } if ( t . prepare ) { request = await t . prepare ( request ) ; } const callback = this . callbackService . create ( ) ; request . setCallback ( callback . url , true ) ; return { request , callback } ; } internal public async sendRequest ( request , callback , chain ? , transport ? , broadcast = false ) { const t = transport || this . transport ; try { const linkUrl = request . data . callback ; if ( linkUrl !== callback . url ) { throw new Error ( 'Invalid request callback' ) ; } if ( request . data . flags . broadcast === true || request . data . flags . background === false ) { throw new Error ( 'Invalid request flags' ) ; } let done = false ; const cancel = new Promise < never > ( ( resolve , reject ) => { t . onRequest ( request , ( reason ) => { if ( done ) { return ; } const error = typeof reason === 'string' ? new CancelError ( reason ) : reason ; if ( t . recoverError && t . recoverError ( error , request ) === true ) { return ; } callback . cancel ( ) ; reject ( error ) ; } ) ; } ) ; const callbackResponse = await Promise . race ( [ callback . wait ( ) , cancel ] ) ; done = true ; if ( typeof callbackResponse . rejected === 'string' ) { throw new CancelError ( callbackResponse . rejected ) ; } const payload = callbackResponse as CallbackPayload ; const signer = PermissionLevel . from ( { actor : payload . sa , permission : payload . sp , } ) ; const signatures = Object . keys ( payload ) . filter ( ( key ) => key . startsWith ( 'sig' ) && key !== 'sig0' ) . map ( ( key ) => Signature . from ( payload [ key ] ! ) ) ; let c ; if ( ! chain && this . chains . length > 1 ) { if ( ! payload . cid ) { throw new Error ( 'Multi chain response payload must specify resolved chain id (cid)' ) ; } c = this . getChain ( payload . cid ) ; } else { c = chain || this . getChain ( 0 ) ; if ( payload . cid && ! c . chainId . equals ( payload . cid ) ) { throw new Error ( 'Got response for wrong chain id' ) ; } } const resolved = await ResolvedSigningRequest . fromPayload ( payload , { zlib , abiProvider : c , } ) ; const cosignerSig = resolved . request . getInfoKey ( 'cosig' , { type : Signature , array : true , } ) as Signature [ ] | undefined ; if ( cosignerSig ) { signatures . unshift ( ... cosignerSig ) ; } const result = { resolved , chain : c , transaction : resolved . transaction , resolvedTransaction : resolved . resolvedTransaction , signatures , payload , signer , } ; if ( broadcast ) { const signedTx = SignedTransaction . from ( { ... resolved . transaction , signatures , } ) ; const res = await c . client . v1 . chain . push_transaction ( signedTx ) ; result . processed = res . processed ; } if ( t . onSuccess ) { t . onSuccess ( request , result ) ; } return result ; } catch ( error ) { if ( t . onFailure ) { t . onFailure ( request , error ) ; } throw error ; } } param args param options param transport public async transact ( args , options ? , transport ? ) { const o = options || { } ; const t = transport || this . transport ; const c = o . chain !== undefined ? this . getChain ( o . chain ) : undefined ; const broadcast = o . broadcast !== false ; const noModify = o . noModify !== undefined ? o . noModify : ! broadcast ; if ( t && t . showLoading ) { t . showLoading ( ) ; } const anyArgs = args as any ; if ( args . actions && ( anyArgs . expiration || anyArgs . ref_block_num || anyArgs . ref_block_prefix || anyArgs . max_net_usage_words || anyArgs . max_cpu_usage_ms || anyArgs . delay_sec ) ) { args = { transaction : { expiration : '1970-01-01T00:00:00' , ref_block_num : 0 , ref_block_prefix : 0 , max_net_usage_words : 0 , max_cpu_usage_ms : 0 , delay_sec : 0 , ... anyArgs , } , } ; } const { request , callback } = await this . createRequest ( args , c , t ) ; if ( noModify ) { request . setInfoKey ( 'no_modify' , true , 'bool' ) ; } const result = await this . sendRequest ( request , callback , c , t , broadcast ) ; return result ; } param args . scope param args . requestPermission param args . info note public async identify ( args ) { const { request , callback } = await this . createRequest ( { identity : { permission : args . requestPermission , scope : args . scope } , info : args . info , } ) ; const res = await this . sendRequest ( request , callback ) ; if ( ! res . resolved . request . isIdentity ( ) ) { throw new IdentityError ( 'Unexpected response' ) ; } let account ; const proof = res . resolved . getIdentityProof ( res . signatures [ 0 ] ) ; if ( this . verifyProofs ) { account = await res . chain . client . v1 . chain . get_account ( res . signer . actor ) ; if ( ! account ) { throw new IdentityError ( `Signature from unknown account: ${ proof . signer . actor }` ) ; } const accountPermission = account . permissions . find ( ( { perm_name } ) => proof . signer . permission . equals ( perm_name ) ) ; if ( ! accountPermission ) { throw new IdentityError ( `${ proof . signer . actor } signed for unknown permission: ${ proof . signer . permission }` ) ; } const proofValid = proof . verify ( accountPermission . required_auth , account . head_block_time ) ; if ( ! proofValid ) { throw new IdentityError ( `Invalid identify proof for: ${ proof . signer }` ) ; } } if ( args . requestPermission ) { const perm = PermissionLevel . from ( args . requestPermission ) ; if ( ( ! perm . actor . equals ( PlaceholderName ) && ! perm . actor . equals ( proof . signer . actor ) ) || ( ! perm . permission . equals ( PlaceholderPermission ) && ! perm . permission . equals ( proof . signer . permission ) ) ) { throw new IdentityError ( `Identity proof singed by ${ proof . signer }, expected: ${ formatAuth ( perm ) } ` ) ; } } return { ... res , account , proof , } ; } param identifier public async login ( identifier ) { const privateKey = PrivateKey . generate ( 'K1' ) ; const requestKey = privateKey . toPublic ( ) ; const createInfo = LinkCreate . from ( { session_name : identifier , request_key : requestKey , user_agent : this . getUserAgent ( ) , } ) ; const res = await this . identify ( { scope : identifier , info : { link : createInfo , scope : identifier , } , } ) ; const metadata = sessionMetadata ( res . payload , res . resolved . request ) ; const signerKey = res . proof . recover ( ) ; let session ; if ( res . payload . link_ch && res . payload . link_key && res . payload . link_name ) { session = new LinkChannelSession ( this , { identifier , chainId : res . chain . chainId , auth : res . signer , publicKey : signerKey , channel : { url : res . payload . link_ch , key : res . payload . link_key , name : res . payload . link_name , } , requestKey : privateKey , } , metadata ) ; } else { session = new LinkFallbackSession ( this , { identifier , chainId : res . chain . chainId , auth : res . signer , publicKey : signerKey , } , metadata ) ; } await this . storeSession ( session ) ; return { ... res , session , } ; } param identifier param auth param chainId returns throws public async restoreSession ( identifier , auth ? , chainId ? ) { if ( ! this . storage ) { throw new Error ( 'Unable to restore session: No storage adapter configured' ) ; } let key ; if ( auth && chainId ) { key = this . sessionKey ( identifier , formatAuth ( PermissionLevel . from ( auth ) ) , String ( ChainId . from ( chainId ) ) ) ; } else { let list = await this . listSessions ( identifier ) ; if ( auth ) { list = list . filter ( ( item ) => item . auth . equals ( auth ) ) ; } if ( chainId ) { const id = ChainId . from ( chainId ) ; list = list . filter ( ( item ) => item . chainId . equals ( id ) ) ; } const latest = list [ 0 ] ; if ( ! latest ) { return null ; } key = this . sessionKey ( identifier , formatAuth ( latest . auth ) , String ( latest . chainId ) ) ; } const data = await this . storage . read ( key ) ; if ( ! data ) { return null ; } let sessionData ; try { sessionData = JSON . parse ( data ) ; } catch ( error ) { throw new Error ( `Unable to restore session: Stored JSON invalid (${ error . message || String ( error ) })` ) ; } const session = LinkSession . restore ( this , sessionData ) ; if ( auth || chainId ) { await this . touchSession ( identifier , session . auth , session . chainId ) ; } return session ; } throws public async listSessions ( identifier ) { if ( ! this . storage ) { throw new Error ( 'Unable to list sessions: No storage adapter configured' ) ; } const key = this . sessionKey ( identifier , 'list' ) ; let list ; try { list = JSON . parse ( ( await this . storage . read ( key ) ) || '[]' ) ; } catch ( error ) { throw new Error ( `Unable to list sessions: ${ error . message || String ( error ) }` ) ; } return list . map ( ( { auth , chainId } ) => ( { auth : PermissionLevel . from ( auth ) , chainId : ChainId . from ( chainId ) , } ) ) ; } throws public async removeSession ( identifier , auth , chainId ) { if ( ! this . storage ) { throw new Error ( 'Unable to remove session: No storage adapter configured' ) ; } const key = this . sessionKey ( identifier , formatAuth ( auth ) , String ( chainId ) ) ; await this . storage . remove ( key ) ; await this . touchSession ( identifier , auth , chainId , true ) ; } throws public async clearSessions ( identifier ) { if ( ! this . storage ) { throw new Error ( 'Unable to clear sessions: No storage adapter configured' ) ; } for ( const { auth , chainId } of await this . listSessions ( identifier ) ) { await this . removeSession ( identifier , auth , chainId ) ; } } param availableKeys param chain param transport note public makeSignatureProvider ( availableKeys , chain ? , transport ? ) { return { getAvailableKeys : async ( ) => availableKeys , sign : async ( args ) => { const t = transport || this . transport ; const c = chain ? this . getChain ( chain ) : this . chains [ 0 ] ; let request = SigningRequest . fromTransaction ( args . chainId , args . serializedTransaction , { abiProvider : c , zlib } ) ; const callback = this . callbackService . create ( ) ; request . setCallback ( callback . url , true ) ; request . setBroadcast ( false ) ; if ( t . prepare ) { request = await t . prepare ( request ) ; } const { transaction , signatures } = await this . sendRequest ( request , callback , c , t ) ; const serializedTransaction = Serializer . encode ( { object : transaction } ) ; return { ... args , serializedTransaction , signatures , } ; } , } ; } /** Makes sure session is in storage list of sessions and moves it to top (most recently used). */ private async touchSession ( identifier , auth , chainId , remove = false ) { const list = await this . listSessions ( identifier ) ; const existing = list . findIndex ( ( item ) => item . auth . equals ( auth ) && item . chainId . equals ( chainId ) ) ; if ( existing >= 0 ) { list . splice ( existing , 1 ) ; } if ( remove === false ) { list . unshift ( { auth , chainId } ) ; } const key = this . sessionKey ( identifier , 'list' ) ; await this . storage ! . write ( key , JSON . stringify ( list ) ) ; } internal async storeSession ( session ) { if ( this . storage ) { const key = this . sessionKey ( session . identifier , formatAuth ( session . auth ) , String ( session . chainId ) ) ; const data = JSON . stringify ( session . serialize ( ) ) ; await this . storage . write ( key , data ) ; await this . touchSession ( session . identifier , session . auth , session . chainId ) ; } } /** Session storage key for identifier and suffix. */ private sessionKey ( identifier , ... suffix ) { return [ String ( Name . from ( identifier ) ) , ... suffix ] . join ( '-' ) ; } internal getUserAgent ( ) { let rv = `AnchorLink/${ Link . version }` ; if ( this . transport . userAgent ) { rv += ' ' + this . transport . userAgent ( ) ; } return rv ; } } internal function formatAuth ( auth ) { const a = PermissionLevel . from ( auth ) ; const actor = a . actor . equals ( PlaceholderName ) ? '<any>' : String ( a . actor ) ; let permission ; if ( a . permission . equals ( PlaceholderName ) || a . permission . equals ( PlaceholderPermission ) ) { permission = '<any>' ; } else { permission = String ( a . permission ) ; } return `${ actor }@${ permission }` ; }