import { AccountID , Address } from './account.js' ; import { BlockHash , BlockHeight , BlockID , BlockOptions , BlockProxy , parseBlockID , } from './block.js' ; import { NETWORKS } from './config.js' ; import { KeyStore } from './key_store.js' ; import { Err , Ok , Quantity , Result , U256 } from './prelude.js' ; import { SubmitResult , FunctionCallArgs , GetStorageAtArgs , InitCallArgs , NewCallArgs , ViewCallArgs , FungibleTokenMetadata , TransactionStatus , OutOfGas , } from './schema.js' ; import { TransactionID } from './transaction.js' ; import { defaultAbiCoder } from '@ethersproject/abi' ; import { arrayify as parseHexString } from '@ethersproject/bytes' ; import { parse as parseRawTransaction } from '@ethersproject/transactions' ; import { toBigIntBE , toBufferBE } from 'bigint-buffer' ; import { Buffer } from 'buffer' ; import BN from 'bn.js' ; import * as NEAR from 'near-api-js' ; import { ResErr } from '@hqoss/monads/dist/lib/result/result' ; export { getAddress as parseAddress } from '@ethersproject/address' ; export { arrayify as parseHexString } from '@ethersproject/bytes' ; export type Bytecode = Uint8Array ; export type Bytecodeish = Bytecode | string ; export type ChainID = bigint ; export type Error = string ; export interface TransactionOutcome { id : TransactionID ; output : Uint8Array ; } export interface BlockInfo { hash : BlockHash ; coinbase : Address ; timestamp : number ; number : BlockHeight ; difficulty : number ; gasLimit : Quantity ; } export interface ViewOptions { block ? : BlockID ; } export interface ConnectOptions { network ? : string ; endpoint ? : string ; contract ? : string ; signer ? : string ; } export interface ConnectEnv { AURORA_ENGINE ? : string ; HOME ? : string ; NEAR_ENV ? : string ; NEAR_MASTER_ACCOUNT ? : string ; NEAR_URL ? : string ; } export type AddressStorage = Map < U256 , U256 > ; export class AddressState { constructor ( public address , public nonce = BigInt ( 0 ) , public balance = BigInt ( 0 ) , public code ? , public storage = new Map ( ) ) { } } export const enum EngineStorageKeyPrefix { Config = 0x0 , Nonce = 0x1 , Balance = 0x2 , Code = 0x3 , Storage = 0x4 } export type EngineStorage = Map < Address , AddressState > ; export class EngineState { constructor ( public storage = new Map ( ) ) { } } const DEFAULT_NETWORK_ID = 'local' ; export class Engine { protected constructor ( public readonly near , public readonly keyStore , public readonly signer , public readonly networkID , public readonly contractID ) { } static async connect ( options , env ? ) { const networkID = options . network || ( env && env . NEAR_ENV ) || DEFAULT_NETWORK_ID ; const network = NETWORKS . get ( networkID ) ! ; const contractID = AccountID . parse ( options . contract || ( env && env . AURORA_ENGINE ) || network . contractID ) . unwrap ( ) ; const signerID = AccountID . parse ( options . signer || ( env && env . NEAR_MASTER_ACCOUNT ) ) . unwrap ( ) ; const keyStore = KeyStore . load ( networkID , env ) ; const near = new NEAR . Near ( { deps : { keyStore } , networkId : networkID , nodeUrl : options . endpoint || ( env && env . NEAR_URL ) || network . nearEndpoint , } ) ; const signer = await near . account ( signerID . toString ( ) ) ; return new Engine ( near , keyStore , signer , networkID , contractID ) ; } async install ( contractCode ) { const contractAccount = ( await this . getAccount ( ) ) . unwrap ( ) ; const result = await contractAccount . deployContract ( contractCode ) ; return Ok ( TransactionID . fromHex ( result . transaction . hash ) ) ; } async upgrade ( contractCode ) { return await this . install ( contractCode ) ; } async initialize ( options ) { const newArgs = new NewCallArgs ( parseHexString ( defaultAbiCoder . encode ( [ 'uint256' ] , [ options . chain || 0 ] ) ) , options . owner || '' , options . bridgeProver || '' , new BN ( options . upgradeDelay || 0 ) ) ; const default_ft_metadata = FungibleTokenMetadata . default ( ) ; const given_ft_metadata = options . metadata || default_ft_metadata ; const ft_metadata = new FungibleTokenMetadata ( given_ft_metadata . spec || default_ft_metadata . spec , given_ft_metadata . name || default_ft_metadata . name , given_ft_metadata . symbol || default_ft_metadata . symbol , given_ft_metadata . icon || default_ft_metadata . icon , given_ft_metadata . reference || default_ft_metadata . reference , given_ft_metadata . reference_hash || default_ft_metadata . reference_hash , given_ft_metadata . decimals || default_ft_metadata . decimals ) ; const connectorArgs = new InitCallArgs ( options . prover || 'prover.ropsten.testnet' , options . ethCustodian || '9006a6D7d08A388Eeea0112cc1b6b6B15a4289AF' , ft_metadata ) ; const tx = await this . promiseAndThen ( this . callMutativeFunction ( 'new' , newArgs . encode ( ) ) , ( _ ) => this . callMutativeFunction ( 'new_eth_connector' , connectorArgs . encode ( ) ) ) ; return tx . map ( ( { id } ) => id ) ; } private async promiseAndThen < T , U , E > ( p , f ) { const r = await p ; if ( r . isOk ( ) ) { const t = r . unwrap ( ) ; return await f ( t ) ; } else { return Err ( r . unwrapErr ( ) ) ; } } async getAccount ( ) { return Ok ( await this . near . account ( this . contractID . toString ( ) ) ) ; } async getBlockHash ( ) { const contractAccount = ( await this . getAccount ( ) ) . unwrap ( ) ; const state = ( await contractAccount . state ( ) ) as any ; return Ok ( state . block_hash ) ; } async getBlockHeight ( ) { const contractAccount = ( await this . getAccount ( ) ) . unwrap ( ) ; const state = ( await contractAccount . state ( ) ) as any ; return Ok ( state . block_height ) ; } async getBlockInfo ( ) { return Ok ( { hash : '' , coinbase : Address . zero ( ) , timestamp : 0 , number : 0 , difficulty : 0 , gasLimit : 0 , } ) ; } async getBlockTransactionCount ( blockID ) { try { const provider = this . near . connection . provider ; const block = ( await provider . block ( parseBlockID ( blockID ) ) ) as any ; const chunk_mask = block . header . chunk_mask ; const requests = ( block . chunks as any [ ] ) . filter ( ( _ , index ) => chunk_mask [ index ] ) . map ( async ( chunkHeader ) => { if ( chunkHeader . tx_root == '11111111111111111111111111111111' ) { return 0 ; } else { const chunk = await provider . chunk ( chunkHeader . chunk_hash ) ; return chunk . transactions . length ; } } ) ; const counts = ( await Promise . all ( requests ) ) as number [ ] ; return Ok ( counts . reduce ( ( a , b ) => a + b , 0 ) ) ; } catch ( error ) { return Err ( error . message ) ; } } async getBlock ( blockID , options ? ) { const provider = this . near . connection . provider ; return await BlockProxy . fetch ( provider , blockID , options ) ; } async hasBlock ( blockID ) { const provider = this . near . connection . provider ; return await BlockProxy . lookup ( provider , blockID ) ; } async getCoinbase ( ) { return Ok ( Address . zero ( ) ) ; } async getVersion ( options ? ) { return ( await this . callFunction ( 'get_version' , undefined , options ) ) . map ( ( output ) => output . toString ( ) ) ; } async getOwner ( options ? ) { return ( await this . callFunction ( 'get_owner' , undefined , options ) ) . andThen ( ( output ) => AccountID . parse ( output . toString ( ) ) ) ; } async getBridgeProvider ( options ? ) { return ( await this . callFunction ( 'get_bridge_provider' , undefined , options ) ) . andThen ( ( output ) => AccountID . parse ( output . toString ( ) ) ) ; } async getChainID ( options ? ) { const result = await this . callFunction ( 'get_chain_id' , undefined , options ) ; return result . map ( toBigIntBE ) ; } async deployCode ( bytecode ) { const args = parseHexString ( bytecode ) ; const outcome = await this . callMutativeFunction ( 'deploy_code' , args ) ; return outcome . map ( ( { output } ) => { const result = SubmitResult . decode ( Buffer . from ( output ) ) ; return Address . parse ( Buffer . from ( result . output ( ) . unwrap ( ) ) . toString ( 'hex' ) ) . unwrap ( ) ; } ) ; } async call ( contract , input ) { const args = new FunctionCallArgs ( contract . toBytes ( ) , this . prepareInput ( input ) ) ; return ( await this . callMutativeFunction ( 'call' , args . encode ( ) ) ) . map ( ( { output } ) => output ) ; } async submit ( input ) { try { const inputBytes = this . prepareInput ( input ) ; try { const rawTransaction = parseRawTransaction ( inputBytes ) ; if ( rawTransaction . gasLimit . toBigInt ( ) < 21000n ) { return Err ( 'ERR_INTRINSIC_GAS' ) ; } } catch ( error ) { return Err ( 'ERR_INVALID_TX' ) ; } return ( await this . callMutativeFunction ( 'submit' , inputBytes ) ) . map ( ( { output } ) => SubmitResult . decode ( Buffer . from ( output ) ) ) ; } catch ( error ) { return Err ( error . message ) ; } } async view ( sender , address , amount , input , options ? ) { const args = new ViewCallArgs ( sender . toBytes ( ) , address . toBytes ( ) , toBufferBE ( BigInt ( amount ) , 32 ) , this . prepareInput ( input ) ) ; const result = await this . callFunction ( 'view' , args . encode ( ) , options ) ; return result . map ( ( output ) => { const status = TransactionStatus . decode ( output ) ; if ( status . success !== undefined ) return status . success . output as Uint8Array ; else if ( status . revert !== undefined ) return status . revert . output as Uint8Array ; else if ( status . outOfGas !== undefined ) return Err ( status . outOfGas ) ; else if ( status . outOfFund !== undefined ) return Err ( status . outOfFund ) ; else if ( status . outOfOffset !== undefined ) return Err ( status . outOfOffset ) ; else if ( status . callTooDeep !== undefined ) return Err ( status . callTooDeep ) ; else return Err ( 'Failed to retrieve data from the contract' ) ; } ) ; } async getCode ( address , options ? ) { const args = address . toBytes ( ) ; return await this . callFunction ( 'get_code' , args , options ) ; } async getBalance ( address , options ? ) { const args = address . toBytes ( ) ; const result = await this . callFunction ( 'get_balance' , args , options ) ; return result . map ( toBigIntBE ) ; } async getNonce ( address , options ? ) { const args = address . toBytes ( ) ; const result = await this . callFunction ( 'get_nonce' , args , options ) ; return result . map ( toBigIntBE ) ; } async getStorageAt ( address , key , options ? ) { const args = new GetStorageAtArgs ( address . toBytes ( ) , parseHexString ( defaultAbiCoder . encode ( [ 'uint256' ] , [ key ] ) ) ) ; const result = await this . callFunction ( 'get_storage_at' , args . encode ( ) , options ) ; return result . map ( toBigIntBE ) ; } async getAuroraErc20Address ( nep141 , options ? ) { const args = Buffer . from ( nep141 . id , 'utf-8' ) ; const result = await this . callFunction ( 'get_erc20_from_nep141' , args , options ) ; return result . map ( ( output ) => { return Address . parse ( output . toString ( 'hex' ) ) . unwrap ( ) ; } ) ; } async getNEP141Account ( erc20 , options ? ) { const args = erc20 . toBytes ( ) ; const result = await this . callFunction ( 'get_nep141_from_erc20' , args , options ) ; return result . map ( ( output ) => { return AccountID . parse ( output . toString ( 'utf-8' ) ) . unwrap ( ) ; } ) ; } async getStorage ( ) { const result = new Map ( ) ; const contractAccount = ( await this . getAccount ( ) ) . unwrap ( ) ; const records = await contractAccount . viewState ( '' , { finality : 'final' } ) ; for ( const record of records ) { const record_type = record . key [ 0 ] ; if ( record_type == EngineStorageKeyPrefix . Config ) continue ; const key = record_type == EngineStorageKeyPrefix . Storage ? record . key . subarray ( 1 , 21 ) : record . key . subarray ( 1 ) ; const address = Buffer . from ( key ) . toString ( 'hex' ) ; if ( ! result . has ( address ) ) { result . set ( address , new AddressState ( Address . parse ( address ) . unwrap ( ) ) ) ; } const state = result . get ( address ) ! ; switch ( record_type ) { case EngineStorageKeyPrefix . Config : break ; case EngineStorageKeyPrefix . Nonce : state . nonce = toBigIntBE ( record . value ) ; break ; case EngineStorageKeyPrefix . Balance : state . balance = toBigIntBE ( record . value ) ; break ; case EngineStorageKeyPrefix . Code : state . code = record . value ; break ; case EngineStorageKeyPrefix . Storage : { state . storage . set ( toBigIntBE ( record . key . subarray ( 21 ) ) , toBigIntBE ( record . value ) ) ; break ; } } } return Ok ( result ) ; } protected async callFunction ( methodName , args ? , options ? ) { const result = await this . signer . connection . provider . query ( { request_type : 'call_function' , account_id : this . contractID . toString ( ) , method_name : methodName , args_base64 : this . prepareInput ( args ) . toString ( 'base64' ) , finality : options ?. block === undefined || options ?. block === null ? 'final' : undefined , block_id : options ?. block !== undefined && options ?. block !== null ? options . block : undefined , } ) ; if ( result . logs && result . logs . length > 0 ) console . debug ( result . logs ) ; return Ok ( Buffer . from ( result . result ) ) ; } protected async callMutativeFunction ( methodName , args ? ) { this . keyStore . reKey ( ) ; const gas = new BN ( '300000000000000' ) ; try { const result = await this . signer . functionCall ( this . contractID . toString ( ) , methodName , this . prepareInput ( args ) , gas ) ; if ( typeof result . status === 'object' && typeof result . status . SuccessValue === 'string' ) { return Ok ( { id : TransactionID . fromHex ( result . transaction . hash ) , output : Buffer . from ( result . status . SuccessValue , 'base64' ) , } ) ; } return Err ( result . toString ( ) ) ; } catch ( error ) { switch ( error ?. type ) { case 'FunctionCallError' : { const errorKind = error ?. kind ?. ExecutionError ; if ( errorKind ) { const errorCode = errorKind . replace ( 'Smart contract panicked: ' , '' ) ; return Err ( errorCode ) ; } return Err ( error . message ) ; } case 'MethodNotFound' : return Err ( error . message ) ; default : console . debug ( error ) ; return Err ( error . toString ( ) ) ; } } } private prepareInput ( args ? ) { if ( typeof args === 'undefined' ) return Buffer . alloc ( 0 ) ; if ( typeof args === 'string' ) return Buffer . from ( parseHexString ( args as string ) ) ; return Buffer . from ( args ) ; } }