@yubing744/rooch-sdk
Version:
353 lines (311 loc) • 9.28 kB
text/typescript
// 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)
}
}