Skip to content

Commit

Permalink
Merge pull request #1551 from Phala-Network/feat-jssdk-contract-actions
Browse files Browse the repository at this point in the history
JS-SDK: Contract Actions
  • Loading branch information
Leechael authored Mar 14, 2024
2 parents 4941d92 + 7766792 commit 8c76d0b
Show file tree
Hide file tree
Showing 16 changed files with 338 additions and 82 deletions.
2 changes: 1 addition & 1 deletion frontend/packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@phala/sdk",
"version": "0.6.0-beta.15",
"version": "0.6.0-beta.16",
"description": "Phala Phat Contract JS SDK",
"license": "Apache-2.0",
"author": [
Expand Down
99 changes: 99 additions & 0 deletions frontend/packages/sdk/src/actions/estimateContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { convertWeight } from '@polkadot/api-contract/base/util'
import type { ContractCallOutcome } from '@polkadot/api-contract/types'
import type { ContractExecResult } from '@polkadot/types/interfaces'
import { BN_ZERO } from '@polkadot/util'
import type { OnChainRegistry } from '../OnChainRegistry'
import { phalaTypes } from '../options'
import { InkQueryMessage } from '../pruntime/coders'
import { pinkQuery } from '../pruntime/pinkQuery'
import { WorkerAgreementKey } from '../pruntime/WorkerAgreementKey'
import type { FrameSystemAccountInfo, LooseNumber } from '../types'
import { toAbi } from '../utils/abi/toAbi'
import { BN_MAX_SUPPLY } from '../utils/constants'
import { SendPinkCommandParameters } from './sendPinkCommand'
import { SendPinkQueryParameters } from './sendPinkQuery'

export type EstimateContractParameters<T> = SendPinkQueryParameters<T> & {
contractKey: string
deposit?: LooseNumber
transfer?: LooseNumber
}

export type EstimateContractResult = Omit<ContractCallOutcome, 'output'> & {
request: SendPinkCommandParameters
}

export async function estimateContract(
client: OnChainRegistry,
parameters: EstimateContractParameters<any[]>
): Promise<EstimateContractResult> {
const { address, functionName, provider, deposit, transfer } = parameters
if (!client.workerInfo?.pubkey) {
throw new Error('Worker pubkey not found')
}

const abi = toAbi(parameters.abi)
const args = parameters.args || []

const message = abi.findMessage(functionName)
if (!message) {
throw new Error(`Message not found: ${functionName}`)
}
const encodedArgs = message.toU8a(args)
const inkMessage = InkQueryMessage(address, encodedArgs, deposit || BN_MAX_SUPPLY, transfer, true)
const argument = new WorkerAgreementKey(client.workerInfo.pubkey)

const cert = await provider.signCertificate()

const [clusterBalance, onchainBalance, inkResponse] = await Promise.all([
client.getClusterBalance(provider.address),
client.api.query.system.account<FrameSystemAccountInfo>(provider.address),
pinkQuery(client.phactory, argument, inkMessage.toHex(), cert),
])

if (inkResponse.result.isErr) {
// @FIXME: not sure this is enough as not yet tested
throw new Error(`InkResponse Error: ${inkResponse.result.asErr.toString()}`)
}
if (!inkResponse.result.asOk.isInkMessageReturn) {
// @FIXME: not sure this is enough as not yet tested
throw new Error(`Unexpected InkMessageReturn: ${inkResponse.result.asOk.toJSON()?.toString()}`)
}
const { debugMessage, gasConsumed, gasRequired, result, storageDeposit } = phalaTypes.createType<ContractExecResult>(
'ContractExecResult',
inkResponse.result.asOk.asInkMessageReturn.toString()
)

const { gasPrice } = client.clusterInfo ?? {}
if (!gasPrice) {
throw new Error('No Gas Price or deposit Per Byte from cluster info.')
}

// calculate the total costs
const gasLimit = gasRequired.refTime.toBn()
const storageDepositFee = storageDeposit.isCharge ? storageDeposit.asCharge.toBn() : BN_ZERO
const minRequired = gasLimit.mul(gasPrice).add(storageDepositFee)

// Auto deposit.
let autoDeposit = undefined
if (clusterBalance.free.lt(minRequired)) {
const deposit = minRequired.sub(clusterBalance.free)
if (onchainBalance.data.free.lt(deposit)) {
throw new Error(`Not enough balance to pay for gas and storage deposit: ${minRequired.toNumber()}`)
}
autoDeposit = deposit
}

return {
debugMessage: debugMessage,
gasConsumed: gasConsumed,
gasRequired: gasRequired && !convertWeight(gasRequired).v1Weight.isZero() ? gasRequired : gasConsumed,
result,
storageDeposit,
request: {
...parameters,
gasLimit,
deposit: autoDeposit,
},
}
}
57 changes: 57 additions & 0 deletions frontend/packages/sdk/src/actions/sendPinkCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { convertWeight } from '@polkadot/api-contract/base/util'
import { BN_ZERO, hexAddPrefix, isHex } from '@polkadot/util'
import type { OnChainRegistry } from '../OnChainRegistry'
import { EncryptedInkCommand, PlainInkCommand } from '../pruntime/coders'
import type { AbiLike, AnyProvider, HexString, LooseNumber } from '../types'
import { toAbi } from '../utils/abi/toAbi'
import assert from '../utils/assert'
import { randomHex } from '../utils/hex'

export type SendPinkCommandParameters<TArgs = any[]> = {
address: string
contractKey: string
provider: AnyProvider
abi: AbiLike
functionName: string
args?: TArgs
//
gasLimit: LooseNumber
nonce?: HexString
plain?: boolean
value?: LooseNumber
storageDepositLimit?: LooseNumber
deposit?: LooseNumber
}

export async function sendPinkCommand(client: OnChainRegistry, parameters: SendPinkCommandParameters) {
const { address, contractKey, functionName, provider, plain, value, gasLimit, storageDepositLimit, deposit } =
parameters
if (!client.workerInfo?.pubkey) {
throw new Error('Worker pubkey not found')
}

const abi = toAbi(parameters.abi)
const args = parameters.args || []

parameters.nonce && assert(isHex(parameters.nonce) && parameters.nonce.length === 66, 'Invalid nonce provided')
const nonce = parameters.nonce || hexAddPrefix(randomHex(32))

const message = abi.findMessage(functionName)
if (!message) {
throw new Error(`Message not found: ${functionName}`)
}
const encodedArgs = message.toU8a(args)

const createCommand = plain ? PlainInkCommand : EncryptedInkCommand
const payload = createCommand(
contractKey,
encodedArgs,
nonce,
value,
convertWeight(gasLimit || BN_ZERO).v2Weight,
storageDepositLimit
)
const extrinsic = client.api.tx.phalaPhatContracts.pushContractMessage(address, payload.toHex(), deposit || BN_ZERO)
const result = await provider.send(extrinsic)
return result
}
65 changes: 65 additions & 0 deletions frontend/packages/sdk/src/actions/sendPinkQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ContractExecResult } from '@polkadot/types/interfaces'
import type { Codec } from '@polkadot/types-codec/types'
import type { OnChainRegistry } from '../OnChainRegistry'
import { phalaTypes } from '../options'
import { InkQueryMessage } from '../pruntime/coders'
import { pinkQuery } from '../pruntime/pinkQuery'
import { WorkerAgreementKey } from '../pruntime/WorkerAgreementKey'
import type { AbiLike, AnyProvider } from '../types'
import { toAbi } from '../utils/abi/toAbi'

export type SendPinkQueryParameters<TArgs = any[]> = {
address: string
provider: AnyProvider
abi: AbiLike
functionName: string
args?: TArgs
}

export async function sendPinkQuery<TResult extends Codec = Codec>(
client: OnChainRegistry,
parameters: SendPinkQueryParameters
): Promise<TResult | null> {
const { address, functionName, provider } = parameters
if (!client.workerInfo?.pubkey) {
throw new Error('Worker pubkey not found')
}

const abi = toAbi(parameters.abi)
const args = parameters.args || []

const message = abi.findMessage(functionName)
if (!message) {
throw new Error(`Message not found: ${functionName}`)
}
const encodedArgs = message.toU8a(args)
const inkMessage = InkQueryMessage(address, encodedArgs)
const argument = new WorkerAgreementKey(client.workerInfo.pubkey)

const cert = await provider.signCertificate()
const inkResponse = await pinkQuery(client.phactory, argument, inkMessage.toHex(), cert)

if (inkResponse.result.isErr) {
// @FIXME: not sure this is enough as not yet tested
throw new Error(`InkResponse Error: ${inkResponse.result.asErr.toString()}`)
}
if (!inkResponse.result.asOk.isInkMessageReturn) {
// @FIXME: not sure this is enough as not yet tested
throw new Error(`Unexpected InkMessageReturn: ${inkResponse.result.asOk.toJSON()?.toString()}`)
}
const { result } = phalaTypes.createType<ContractExecResult>(
'ContractExecResult',
inkResponse.result.asOk.asInkMessageReturn.toString()
)
if (result.isErr) {
throw new Error(`ContractExecResult Error: ${result.asErr.toString()}`)
}
if (message.returnType) {
return abi.registry.createTypeUnsafe<TResult>(
message.returnType.lookupName || message.returnType.type,
[result.asOk.data.toU8a(true)],
{ isPedantic: true }
)
}
return null
}
20 changes: 7 additions & 13 deletions frontend/packages/sdk/src/contracts/PinkBlueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ import type { AbiConstructor, BlueprintOptions, ContractCallOutcome } from '@pol
import { type Option } from '@polkadot/types'
import type { AccountId, ContractInstantiateResult, Hash } from '@polkadot/types/interfaces'
import type { IKeyringPair, ISubmittableResult } from '@polkadot/types/types'
import { BN, BN_ZERO, hexAddPrefix, hexToU8a, isUndefined } from '@polkadot/util'
import { sr25519Agreement, sr25519PairFromSeed } from '@polkadot/util-crypto'
import { BN, BN_ZERO, isUndefined } from '@polkadot/util'
import { from } from 'rxjs'
import type { OnChainRegistry } from '../OnChainRegistry'
import { phalaTypes } from '../options'
import { Provider } from '../providers/types'
import type { CertificateData } from '../pruntime/certificate'
import { InkQueryInstantiate } from '../pruntime/coders'
import { pinkQuery } from '../pruntime/pinkQuery'
import type { AbiLike, FrameSystemAccountInfo, InkQueryError, InkResponse } from '../types'
import { WorkerAgreementKey } from '../pruntime/WorkerAgreementKey'
import type { AbiLike, FrameSystemAccountInfo, InkQueryError } from '../types'
import { toAbi } from '../utils/abi/toAbi'
import assert from '../utils/assert'
import { BN_MAX_SUPPLY } from '../utils/constants'
import { randomHex } from '../utils/hex'
Expand Down Expand Up @@ -211,7 +212,7 @@ export class PinkBlueprintPromise {
throw new Error('Your phatRegistry has not been initialized correctly.')
}

this.abi = abi instanceof Abi ? abi : new Abi(abi, api.registry.getChainProperties())
this.abi = toAbi(abi, api.registry.getChainProperties())
this.api = api
this._decorateMethod = toPromiseMethod
this.phatRegistry = phatRegistry
Expand Down Expand Up @@ -299,15 +300,9 @@ export class PinkBlueprintPromise {
options: PinkInstantiateQueryOptions,
params: unknown[]
) => {
// Generate a keypair for encryption
// NOTE: each instance only has a pre-generated pair now, it maybe better to generate a new keypair every time encrypting
const seed = hexToU8a(hexAddPrefix(randomHex(32)))
const pair = sr25519PairFromSeed(seed)
const [sk, pk] = [pair.secretKey, pair.publicKey]
const agreement = new WorkerAgreementKey(this.phatRegistry.remotePubkey!)
const { cert } = options

const queryAgreementKey = sr25519Agreement(sk, hexToU8a(hexAddPrefix(this.phatRegistry.remotePubkey)))

const inkQueryInternal = async (origin: string | AccountId | Uint8Array) => {
if (typeof origin === 'string') {
assert(origin === cert.address, 'origin must be the same as the certificate address')
Expand All @@ -331,8 +326,7 @@ export class PinkBlueprintPromise {
options.deposit,
options.transfer
)
const rawResponse = await pinkQuery(this.phatRegistry.phactory, pk, queryAgreementKey, payload.toHex(), cert)
const response = phalaTypes.createType<InkResponse>('InkResponse', rawResponse)
const response = await pinkQuery(this.phatRegistry.phactory, agreement, payload.toHex(), cert)
if (response.result.isErr) {
return phalaTypes.createType<InkQueryError>('InkQueryError', response.result.asErr.toHex())
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/packages/sdk/src/contracts/PinkCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { OnChainRegistry } from '../OnChainRegistry'
import type { Provider } from '../providers/types'
import type { CertificateData } from '../pruntime/certificate'
import type { AbiLike } from '../types'
import { toAbi } from '../utils/abi/toAbi'
import { PinkBlueprintPromise } from './PinkBlueprint'

export interface PinkCodeSendOptions {
Expand Down Expand Up @@ -130,7 +131,7 @@ export class PinkCodePromise {
throw new Error('Your phatRegistry has not been initialized correctly.')
}

this.abi = abi instanceof Abi ? abi : new Abi(abi, api.registry.getChainProperties())
this.abi = toAbi(abi, api.registry.getChainProperties())
this.api = api
this._decorateMethod = toPromiseMethod
this.phatRegistry = phatRegistry
Expand Down
Loading

0 comments on commit 8c76d0b

Please sign in to comment.