UNPKG

@yubing744/rooch-sdk

Version:
353 lines (311 loc) 9.28 kB
// Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 import { DEFAULT_MAX_GAS_AMOUNT } from '../constants' import { IAccount, CallOption, ISessionKey } from './interface' import { IClient } from '../client' import { IAuthorizer, IAuthorization, PrivateKeyAuth } from '../auth' import { AccountAddress, FunctionId, TypeTag, Arg, StatePageView, IPage } from '../types' import { BcsSerializer } from '../types/bcs' import { RoochTransaction, RoochTransactionData, AccountAddress as BCSAccountAddress, Authenticator, } from '../generated/runtime/rooch_types/mod' import { encodeArg, encodeFunctionCall, addressToListTuple, uint8Array2SeqNumber, addressToSeqNumber, encodeStructTypeTag, } from '../utils' import { Ed25519Keypair } from '../utils/keypairs' const SCOPE_LENGTH = 3 const SCOPE_MODULE_ADDRESSS = 0 const SCOPE_MODULE_NAMES = 1 const SCOPE_FUNCTION_NAMES = 2 /** * Rooch Account * all write calls in here */ export class Account implements IAccount { private readonly client: IClient private readonly address: AccountAddress private readonly authorizer: IAuthorizer public constructor(client: IClient, address: AccountAddress, authorizer: IAuthorizer) { this.client = client this.address = address this.authorizer = authorizer } private async makeAuth(tsData: RoochTransactionData): Promise<IAuthorization> { const payload = (() => { const se = new BcsSerializer() tsData.serialize(se) return se.getBytes() })() return this.authorizer.auth(payload) } private parseStateToSessionKey(data: StatePageView): Array<ISessionKey> { const result = new Array<ISessionKey>() for (const state of data.data as any) { const moveValue = state?.state.decoded_value as any if (moveValue) { const val = moveValue.value result.push({ authentication_key: val.authentication_key, scopes: this.parseScopes(val.scopes), create_time: parseInt(val.create_time), last_active_time: parseInt(val.last_active_time), max_inactive_interval: parseInt(val.max_inactive_interval), } as ISessionKey) } } return result } private parseScopes(data: Array<any>): Array<string> { const result = new Array<string>() for (const scope of data) { result.push(`${scope.module_name}::${scope.module_address}::${scope.function_name}`) } return result } private async getSequenceNumber(): Promise<number> { const resp = await this.client.executeViewFunction({ funcId: '0x3::account::sequence_number', tyArgs: [], args: [ { type: 'Address', value: this.address, }, ], }) if (resp && resp.return_values) { return resp.return_values[0].decoded_value as number } return 0 } /** * Get account address */ public getAddress(): string { return this.address } /** * Run move function by current account * * @param funcId FunctionId the function like '0x3::empty::empty' * @param tyArgs Generic parameter list * @param args parameter list * @param opts Call option */ public async runFunction( funcId: FunctionId, tyArgs: TypeTag[], args: Arg[], opts: CallOption, ): Promise<string> { const number = await this.getSequenceNumber() const bcsArgs = args.map((arg) => encodeArg(arg)) const scriptFunction = encodeFunctionCall(funcId, tyArgs, bcsArgs) const txData = new RoochTransactionData( new BCSAccountAddress(addressToListTuple(this.address)), BigInt(number), BigInt(this.client.getChainId()), BigInt(opts.maxGasAmount ?? DEFAULT_MAX_GAS_AMOUNT), scriptFunction, ) const authResult = await this.makeAuth(txData) const auth = new Authenticator( BigInt(authResult.scheme), uint8Array2SeqNumber(authResult.payload), ) const ts = new RoochTransaction(txData, auth) const payload = (() => { const se = new BcsSerializer() ts.serialize(se) return se.getBytes() })() return this.client.sendRawTransaction(payload) } public async createSessionAccount( scope: Array<string>, maxInactiveInterval: number, opts?: CallOption, ): Promise<IAccount> { const kp = Ed25519Keypair.generate() await this.registerSessionKey( kp.getPublicKey().toRoochAddress(), scope, maxInactiveInterval, opts, ) const auth = new PrivateKeyAuth(kp) return new Account(this.client, this.address, auth) } public async registerSessionKey( authKey: AccountAddress, scopes: Array<string>, maxInactiveInterval: number, opts?: CallOption, ): Promise<void> { const [scopeModuleAddresss, scopeModuleNames, scopeFunctionNames] = scopes .map((scope: string) => { const parts = scope.split('::') if (parts.length !== SCOPE_LENGTH) { throw new Error('invalid scope') } const scopeModuleAddress = parts[SCOPE_MODULE_NAMES] const scopeModuleName = parts[SCOPE_MODULE_ADDRESSS] const scopeFunctionName = parts[SCOPE_FUNCTION_NAMES] return [scopeModuleAddress, scopeModuleName, scopeFunctionName] }) .reduce( (acc: Array<Array<string>>, val: Array<string>) => { acc[0].push(val[SCOPE_MODULE_NAMES]) acc[1].push(val[SCOPE_MODULE_ADDRESSS]) acc[2].push(val[SCOPE_FUNCTION_NAMES]) return acc }, [[], [], []], ) await this.runFunction( '0x3::session_key::create_session_key_with_multi_scope_entry', [], [ { type: { Vector: 'U8' }, value: addressToSeqNumber(authKey), }, { type: { Vector: 'Address' }, value: scopeModuleAddresss, }, { type: { Vector: 'Ascii' }, value: scopeModuleNames, }, { type: { Vector: 'Ascii' }, value: scopeFunctionNames, }, { type: 'U64', value: BigInt(maxInactiveInterval), }, ], opts || { maxGasAmount: 100000000, }, ) } /** * Remove session key * * @param authKey * @param opts */ public async removeSessionKey(authKey: AccountAddress, opts?: CallOption): Promise<string> { return await this.runFunction( '0x3::session_key::remove_session_key_entry', [], [ { type: { Vector: 'U8' }, value: addressToSeqNumber(authKey), }, ], opts || { maxGasAmount: 100000000, }, ) } /** * Query account's sessionKey * * @param cursor The page cursor * @param limit The page limit */ public async querySessionKeys( cursor: string | null, limit: number, ): Promise<IPage<ISessionKey, string>> { const accessPath = `/resource/${this.address}/0x3::session_key::SessionKeys` const state = await this.client.getStates(accessPath) if (state) { const stateView = state as any const tableId = stateView[0].value const accessPath = `/table/${tableId}` const pageView = await this.client.listStates({ accessPath, cursor, limit, }) return { data: this.parseStateToSessionKey(pageView), nextCursor: pageView.next_cursor, hasNextPage: pageView.has_next_page, } } throw new Error('not found state') } /** * Check session key whether expired * * @param authKey the auth key */ async isSessionKeyExpired(authKey: AccountAddress): Promise<boolean> { const result = await this.client.executeViewFunction({ funcId: '0x3::session_key::is_expired_session_key', tyArgs: [], args: [ { type: 'Address', value: this.address, }, { type: { Vector: 'U8' }, value: addressToSeqNumber(authKey), }, ], }) if (result && result.vm_status !== 'Executed') { throw new Error('view 0x3::session_key::is_expired_session_key fail') } return result.return_values![0].decoded_value as boolean } async gasCoinBalance(): Promise<bigint> { const result = await this.client.executeViewFunction({ funcId: '0x3::gas_coin::balance', tyArgs: [], args: [ { type: 'Address', value: this.getAddress(), }, ], }) if (result && result.vm_status !== 'Executed') { throw new Error('view 0x3::gas_coin::balance fail') } return BigInt(result.return_values![0].decoded_value as string) } async coinBalance(coinType: string): Promise<bigint> { const structType = encodeStructTypeTag(coinType) const result = await this.client.executeViewFunction({ funcId: '0x3::account_coin_store::balance', tyArgs: [structType], args: [ { type: 'Address', value: this.getAddress(), }, ], }) if (result && result.vm_status !== 'Executed') { throw new Error('view 0x3::account_coin_store::balance fail') } return BigInt(result.return_values![0].decoded_value as string) } }