UNPKG

@roochnetwork/rooch-sdk

Version:
790 lines (687 loc) 22.6 kB
// Copyright (c) RoochNetwork // SPDX-License-Identifier: Apache-2.0 import { Args } from '../bcs/index.js' import { Signer } from '../crypto/index.js' import { CreateSessionArgs, Session } from '../session/index.js' import { decodeToRoochAddressStr, decodeToPackageAddressStr, BitcoinAddress, BitcoinNetowkType, RoochAddress, } from '../address/index.js' import { address, Bytes, u64 } from '../types/index.js' import { fromHEX, str } from '../utils/index.js' import { RoochTransport } from './transportInterface.js' import { RoochHTTPTransport } from './httpTransport.js' import { CallFunction, CallFunctionArgs, TypeArgs, Transaction, normalizeTypeArgsToStr, } from '../transactions/index.js' import { AnnotatedFunctionResultView, BalanceInfoView, ExecuteTransactionResponseView, GetBalanceParams, GetStatesParams, ListStatesParams, PaginatedStateKVViews, PaginationArguments, PaginationResult, SessionInfoView, ObjectStateView, QueryUTXOsParams, PaginatedUTXOStateViews, PaginatedInscriptionStateViews, QueryInscriptionsParams, GetBalancesParams, PaginatedBalanceInfoViews, QueryObjectStatesParams, PaginatedIndexerObjectStateViews, QueryTransactionsParams, PaginatedTransactionWithInfoViews, PaginatedEventViews, GetEventsByEventHandleParams, QueryEventsParams, PaginatedIndexerEventViews, ModuleABIView, GetModuleABIParams, BroadcastTXParams, GetObjectStatesParams, GetFieldStatesParams, ListFieldStatesParams, GetTransactionsByHashParams, TransactionWithInfoView, GetTransactionsByOrderParams, RepairIndexerParams, SyncStatesParams, PaginatedStateChangeSetWithTxOrderViews, DryRunRawTransactionParams, DryRunTransactionResponseView, EventFilterView, TransactionFilterView, IndexerEventView, } from './types/index.js' import { fixedBalance } from '../utils/balance.js' const DEFAULT_GAS = 50000000 // const logger = createLogger('client') /** * Configuration options for the RoochClient * You must provide either a `url` or a `transport` */ export type RoochClientOptions = NetworkOrTransport type NetworkOrTransport = | { url: string transport?: never // subscriptionTransport?: never } | { transport: RoochTransport // subscriptionTransport?: RoochSubscriptionTransport url?: never } const ROOCH_CLIENT_BRAND = Symbol.for('@roochnetwork/RoochClient') export function isRoochClient(client: unknown): client is RoochClient { return ( typeof client === 'object' && client !== null && (client as { [ROOCH_CLIENT_BRAND]: unknown })[ROOCH_CLIENT_BRAND] === true ) } // type SubscriptionEvent = // | { type: 'event'; data: IndexerEventView } // | { type: 'transaction'; data: TransactionWithInfoView } export interface SubscriptionEventParams { filter?: EventFilterView onError?: (error: Error) => void signal?: AbortSignal } export interface SubscriptionTransactionParams { filter?: TransactionFilterView onError?: (error: Error) => void signal?: AbortSignal } export type Unsubscribe = () => Promise<boolean> export class RoochClient { protected chainID: bigint | undefined protected transport: RoochTransport get [ROOCH_CLIENT_BRAND]() { return true } getTransport() { return this.transport } /** * Establish a connection to a rooch RPC endpoint * * @param options configuration options for the API Client */ constructor(options: RoochClientOptions) { this.transport = options.transport ?? new RoochHTTPTransport({ url: options.url }) } async getRpcApiVersion(): Promise<string | undefined> { const resp = await this.transport.request<{ info: { version: string } }>({ method: 'rpc.discover', params: [], }) return resp.info.version } async getChainId(): Promise<u64> { if (this.chainID) { return this.chainID } return this.transport.request({ method: 'rooch_getChainID', params: [], }) } async executeViewFunction(input: CallFunctionArgs): Promise<AnnotatedFunctionResultView> { const callFunction = new CallFunction(input) return await this.transport.request({ method: 'rooch_executeViewFunction', params: [ { function_id: callFunction.functionId(), args: callFunction.encodeArgs(), ty_args: callFunction.typeArgs, }, ], }) } async dryrun(input: DryRunRawTransactionParams): Promise<DryRunTransactionResponseView> { return await this.transport.request({ method: 'rooch_dryRunRawTransaction', params: [input.txBcsHex], }) } async signAndExecuteTransaction({ transaction, signer, option = { withOutput: true }, }: { transaction: Transaction | Bytes signer: Signer option?: { withOutput: boolean } }): Promise<ExecuteTransactionResponseView> { let transactionHex: string if (transaction instanceof Uint8Array) { transactionHex = str('hex', transaction) } else { let sender = signer.getRoochAddress().toHexAddress() transaction.setChainId(await this.getChainId()) transaction.setSeqNumber(await this.getSequenceNumber(sender)) transaction.setSender(sender) // need dry_run if (!transaction.getMaxGas()) { transaction.setMaxGas(DEFAULT_GAS) // const s = transaction.encodeData().toHex() // const result = await this.dryrun({ txBcsHex: s }) // // if (result.raw_output.status.type === 'executed') { // transaction.setMaxGas(Math.ceil(Number(result.raw_output.gas_used) * 100)) // } else { // // TODO: abort? // throw Error(result.raw_output.status.type) // } } const auth = await signer.signTransaction(transaction) transaction.setAuth(auth) transactionHex = `0x${transaction.encode().toHex()}` } return await this.transport.request({ method: 'rooch_executeRawTransaction', params: [transactionHex, option], }) } async repairIndexer(input: RepairIndexerParams) { await this.transport.request({ method: 'rooch_repairIndexer', params: [input.repairType, input.repairParams], }) } async syncStates(input: SyncStatesParams): Promise<PaginatedStateChangeSetWithTxOrderViews> { const opt = input.queryOption || { decode: true, showDisplay: true, } return await this.transport.request({ method: 'rooch_syncStates', params: [input.filter, input.cursor, input.limit, opt], }) } // Get the states by access_path async getStates(input: GetStatesParams): Promise<ObjectStateView[]> { const opt = input.stateOption || { decode: true, showDisplay: true, } const result = await this.transport.request({ method: 'rooch_getStates', params: [input.accessPath, opt], }) const typedResult = result as unknown as ObjectStateView[] return typedResult[0] === null ? [] : typedResult } async listStates(input: ListStatesParams): Promise<PaginatedStateKVViews> { const opt = input.stateOption || { decode: true, showDisplay: true, } return await this.transport.request({ method: 'rooch_listStates', params: [input.accessPath, input.cursor, input.limit, opt], }) } async getModuleAbi(input: GetModuleABIParams): Promise<ModuleABIView> { return await this.transport.request({ method: 'rooch_getModuleABI', params: [input.moduleAddr, input.moduleName], }) } async getEvents(input: GetEventsByEventHandleParams): Promise<PaginatedEventViews> { const opt = input.eventOptions || { decode: true, } return await this.transport.request({ method: 'rooch_getEventsByEventHandle', params: [input.eventHandle, input.cursor, input.limit, input.descendingOrder, opt], }) } async queryEvents(input: QueryEventsParams): Promise<PaginatedIndexerEventViews> { if (typeof input.filter === 'object' && 'sender' in input.filter) { if (input.filter.sender === '') { throw Error('Invalid Address') } } if (typeof input.filter === 'object' && 'event_type_with_sender' in input.filter) { if (input.filter.event_type_with_sender.sender === '') { throw Error('Invalid Address') } } const opt = input.queryOption || { decode: true, showDisplay: true, } return await this.transport.request({ method: 'rooch_queryEvents', params: [input.filter, input.cursor, input.limit, opt], }) } async queryInscriptions(input: QueryInscriptionsParams): Promise<PaginatedInscriptionStateViews> { if (typeof input.filter !== 'string' && 'owner' in input.filter) { if (input.filter.owner === '') { throw Error('Invalid Address') } } return await this.transport.request({ method: 'btc_queryInscriptions', params: [input.filter, input.cursor, input.limit, input.descendingOrder], }) } async queryUTXO(input: QueryUTXOsParams): Promise<PaginatedUTXOStateViews> { if (typeof input.filter !== 'string' && 'owner' in input.filter) { if (input.filter.owner === '') { throw Error('Invalid Address') } } return this.transport.request({ method: 'btc_queryUTXOs', params: [input.filter, input.cursor, input.limit, input.descendingOrder], }) } async broadcastBitcoinTX(input: BroadcastTXParams): Promise<string> { return this.transport.request({ method: 'btc_broadcastTX', params: [input.hex, input.maxfeerate, input.maxburnamount], }) } async getObjectStates(input: GetObjectStatesParams): Promise<ObjectStateView[]> { const idsStr = input.ids.join(',') const opt = input.stateOption || { decode: true, showDisplay: true, } return this.transport.request({ method: 'rooch_getObjectStates', params: [idsStr, opt], }) } async getFieldStates(input: GetFieldStatesParams): Promise<ObjectStateView[]> { const opt = input.stateOption || { decode: true, showDisplay: true, } return this.transport.request({ method: 'rooch_getFieldStates', params: [input.objectId, input.fieldKey, opt], }) } async listFieldStates(input: ListFieldStatesParams): Promise<PaginatedStateKVViews> { const opt = input.stateOption || { decode: true, showDisplay: true, } return this.transport.request({ method: 'rooch_listFieldStates', params: [input.objectId, input.cursor, input.limit, opt], }) } async queryObjectStates( input: QueryObjectStatesParams, ): Promise<PaginatedIndexerObjectStateViews> { if ('owner' in input.filter) { if (input.filter.owner === '') { throw Error('Invalid Address') } } if ('object_type_with_owner' in input.filter) { if (input.filter.object_type_with_owner.owner === '') { throw Error('Invalid Address') } } const opt = input.queryOption || { decode: true, showDisplay: true, } return this.transport.request({ method: 'rooch_queryObjectStates', params: [input.filter, input.cursor, input.limit, opt], }) } async getTransactionsByHash( input: GetTransactionsByHashParams, ): Promise<TransactionWithInfoView> { return this.transport.request({ method: 'rooch_getTransactionsByHash', params: [input.txHashes], }) } async getTransactionsByOrder( input: GetTransactionsByOrderParams, ): Promise<PaginatedTransactionWithInfoViews> { return this.transport.request({ method: 'rooch_queryTransactions', params: [input.cursor, input.limit, input.descendingOrder], }) } async queryTransactions( input: QueryTransactionsParams, ): Promise<PaginatedTransactionWithInfoViews> { if (typeof input.filter === 'object' && 'sender' in input.filter) { if (input.filter.sender === '') { throw Error('Invalid Address') } } const opt = input.queryOption || { decode: true, showDisplay: true, } return this.transport.request({ method: 'rooch_queryTransactions', params: [input.filter, input.cursor, input.limit, opt], }) } // helper fn async getSequenceNumber(address: string): Promise<u64> { const resp = await this.executeViewFunction({ target: '0x2::account::sequence_number', args: [Args.address(address)], }) if (resp && resp.return_values) { return BigInt(resp.return_values?.[0]?.decoded_value as number) } return BigInt(0) } /** * Get the total coin balance for one coin type, owned by the address owner. */ async getBalance(input: GetBalanceParams): Promise<BalanceInfoView> { const owner = decodeToRoochAddressStr(input.owner) let balanceInfoView: BalanceInfoView = await this.transport.request({ method: 'rooch_getBalance', params: [owner, input.coinType], }) balanceInfoView.fixedBalance = fixedBalance(balanceInfoView.balance, balanceInfoView.decimals) return balanceInfoView } async getBalances(input: GetBalancesParams): Promise<PaginatedBalanceInfoViews> { const owner = decodeToRoochAddressStr(input.owner) // balanceInfoView.fixedBalance = fixedBalance(balanceInfoView.balance, balanceInfoView.decimals) const result: PaginatedBalanceInfoViews = await this.transport.request({ method: 'rooch_getBalances', params: [owner, input.cursor, input.limit], }) result.data.forEach((item) => { item.fixedBalance = fixedBalance(item.balance, item.decimals) }) return result } async transfer(input: { signer: Signer recipient: address amount: number | bigint coinType: TypeArgs }) { const recipient = decodeToRoochAddressStr(input.recipient) const tx = new Transaction() tx.callFunction({ target: '0x3::transfer::transfer_coin', args: [Args.address(recipient), Args.u256(BigInt(input.amount))], typeArgs: [normalizeTypeArgsToStr(input.coinType)], }) return await this.signAndExecuteTransaction({ transaction: tx, signer: input.signer, }) } async transferObject(input: { signer: Signer recipient: address objectId: string objectType: TypeArgs }) { const recipient = decodeToRoochAddressStr(input.recipient) const tx = new Transaction() tx.callFunction({ target: '0x3::transfer::transfer_object', args: [Args.address(recipient), Args.objectId(input.objectId)], typeArgs: [normalizeTypeArgsToStr(input.objectType)], }) return await this.signAndExecuteTransaction({ transaction: tx, signer: input.signer, }) } async resolveBTCAddress(input: { roochAddress: string | RoochAddress network: BitcoinNetowkType }): Promise<BitcoinAddress | undefined> { const address = decodeToRoochAddressStr(input.roochAddress) const result = await this.executeViewFunction({ target: '0x3::address_mapping::resolve_bitcoin', args: [Args.address(address)], }) if (result.vm_status === 'Executed' && result.return_values) { const value = (result.return_values?.[0]?.decoded_value as { value: any }).value const address = value && value.vec ? //compatible with old option version (((value as any).vec as any).value[0] as Array<string>)[0] : ((value as any).bytes as string) return new BitcoinAddress(address, input.network) } return undefined } async createSession({ sessionArgs, signer }: { sessionArgs: CreateSessionArgs; signer: Signer }) { return Session.CREATE({ ...sessionArgs, client: this, signer: signer, }) } async removeSession({ authKey, signer }: { authKey: string; signer: Signer }): Promise<boolean> { const tx = new Transaction() tx.callFunction({ target: '0x3::session_key::remove_session_key_entry', args: [Args.vec('u8', Array.from(fromHEX(authKey)))], }) return ( ( await this.signAndExecuteTransaction({ transaction: tx, signer, }) ).execution_info.status.type === 'executed' ) } async sessionIsExpired({ address, authKey, }: { address: address authKey: string }): Promise<boolean> { const _address = decodeToRoochAddressStr(address) const result = await this.executeViewFunction({ target: '0x3::session_key::is_expired_session_key', args: [Args.address(_address), Args.vec('u8', Array.from(fromHEX(authKey)))], }) if (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 getAllModules({ package_address, limit, cursor, }: { package_address: address } & PaginationArguments<string>): Promise<Map<string, string>> { const packageObjectID = `0x14481947570f6c2f50d190f9a13bf549ab2f0c9debc41296cd4d506002379659${decodeToPackageAddressStr(package_address)}` const result = await this.transport.request({ method: 'rooch_listFieldStates', params: [packageObjectID, cursor, limit, { decode: true }], }) const moduleInfo = result as unknown as ObjectStateView[] const moduleMap = new Map<string, string>() if (moduleInfo && typeof moduleInfo === 'object' && 'data' in moduleInfo) { const { data } = moduleInfo if (Array.isArray(data)) { for (const item of data) { const decodedValue = item?.state?.decoded_value if (decodedValue) { const name = decodedValue?.value?.name const byte_codes = decodedValue?.value?.value?.value?.byte_codes if (name && byte_codes) { moduleMap.set(name, byte_codes) } } } } } return moduleMap } async getSessionKeys({ address, limit, cursor, }: { address: address } & PaginationArguments<string>): Promise<PaginationResult<string, SessionInfoView>> { const _address = decodeToRoochAddressStr(address) const accessPath = `/resource/${_address}/0x3::session_key::SessionKeys` const states = await this.getStates({ accessPath, stateOption: { decode: true, showDisplay: true, }, }) if (states.length === 0) { return { data: [], hasNextPage: false, } } // Maybe we should define the type? const tableId = ( (((states?.[0]?.decoded_value as any).value['value'] as any).value['keys'] as any).value[ 'handle' ] as any ).value['id'] as string const tablePath = `/table/${tableId}` const statePage = await this.listStates({ accessPath: tablePath, cursor, limit: limit?.toString(), stateOption: { decode: true, showDisplay: true, }, }) const parseScopes = (data: Array<any>) => { const result = new Array<string>() for (const scope of data) { const [pkg, mod, fn] = [scope[0], scope[1], scope[2]] result.push(`${pkg}::${mod}::${fn}`) } return result } const parseStateToSessionInfo = () => { const result = new Array<SessionInfoView>() for (const state of statePage.data as any) { const moveValue = state?.state?.decoded_value as any if (moveValue) { const val = moveValue.value.value.value result.push({ appName: val.app_name, appUrl: val.app_url, authenticationKey: val.authentication_key, scopes: parseScopes(val.scopes.value), createTime: parseInt(val.create_time), lastActiveTime: parseInt(val.last_active_time), maxInactiveInterval: parseInt(val.max_inactive_interval), } as SessionInfoView) } } return result.sort((a, b) => b.createTime - a.createTime) } return { data: parseStateToSessionInfo(), cursor: statePage.next_cursor, hasNextPage: statePage.has_next_page, } } async subscribeEventWithSSE( input: SubscriptionEventParams & { /** function to run when we receive a notification of a new event matching the filter */ onMessage: (event: IndexerEventView) => void }, ): Promise<() => Promise<boolean>> { const params = input.filter ? (input.filter as any) : 'all' return this.transport.subscribeWithSSE({ method: '/subscribe/sse/events', params: params, onMessage: input.onMessage, onError: input.onError, signal: input.signal, }) } async subscribeTransactionWithSSE( input: SubscriptionEventParams & { /** function to run when we receive a notification of a new event matching the filter */ onMessage: (event: IndexerEventView) => void }, ): Promise<() => Promise<boolean>> { const params = input.filter ? (input.filter as any) : 'all' return this.transport.subscribeWithSSE({ method: '/subscribe/sse/transactions', params: params, onMessage: input.onMessage, onError: input.onError, signal: input.signal, }) } async subscribeEvent( input: SubscriptionEventParams & { /** function to run when we receive a notification of a new event matching the filter */ onMessage: (event: IndexerEventView) => void }, ): Promise<() => Promise<boolean>> { const params = input.filter ? [input.filter as any] : ['all'] return this.transport.subscribe({ method: 'rooch_subscribeEvents', params: params, onMessage: input.onMessage, signal: input.signal, }) } async subscribeTransaction( input: SubscriptionTransactionParams & { /** function to run when we receive a notification of a new event matching the filter */ onMessage: (message: TransactionWithInfoView) => void }, ): Promise<() => Promise<boolean>> { const params = input.filter ? [input.filter as any] : ['all'] return this.transport.subscribe({ method: 'rooch_subscribeTransactions', params: params, onMessage: input.onMessage, signal: input.signal, }) } events(): void { throw new Error('Method not implemented. Use getEvents() or queryEvents() instead.') } destroy(): void { this.transport.destroy() // this.subscriptions.clear() } }