Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Post DeveloperDAO Hackathon Improvements #1386

Merged
merged 40 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c3f8337
feat(jssdk): move InkQuery & InkCommand into seperated file and added…
Leechael Sep 14, 2023
590bdfc
imp(jssdk): handling PrpcError
Leechael Sep 15, 2023
d982ade
feat(jssdk): decode SerMessageOuput
Leechael Sep 15, 2023
05bee9b
chore(jssdk): fix typo
Leechael Sep 15, 2023
ef018aa
imp(jssdk): use hexToU8a
Leechael Sep 15, 2023
c41083b
imp(jssdk): handle server-side 500 errors that are not from prpc.
Leechael Sep 15, 2023
18f8979
format(jssdk): apply the suggestion changes from linter & prettier
Leechael Sep 15, 2023
768e8e2
feat(jssdk): add custom predicate function support for the waitFinali…
Leechael Sep 15, 2023
d87c9b4
feat(jssdk): add PinkContractPromise.send which consider as better al…
Leechael Sep 16, 2023
95b6d12
chore(jssdk): prettier & linter
Leechael Sep 16, 2023
b9b700c
chore(jssdk): bump beta version to 0.5.3-nightly-20230916
Leechael Sep 16, 2023
24d63f1
imp(jssdk): handling 400 from PRuntime as well
Leechael Sep 20, 2023
7c6ec43
fix(jssdk): incorrect gas fee estimation in auto-deposit
Leechael Sep 20, 2023
a4f24f5
chore(jssdk): bump beta version to 0.5.3-nightly-20230920
Leechael Sep 20, 2023
421fce9
fix(jssdk): incorrect typing for PinkBlueprintDeploy functions
Leechael Sep 26, 2023
dd614ac
chore(jssdk): bump beta version to 0.5.3-nightly-20230926-1
Leechael Sep 26, 2023
d5e4f45
imp(jssdk): added SignAndSendError
Leechael Sep 26, 2023
e8bcf49
feat(jssdk): added send to PinkBlueprintPromise & PinkContractPromise…
Leechael Sep 26, 2023
0e5d52b
chore(jssdk): bump beta version to 0.5.3-nightly-20230926-4
Leechael Sep 26, 2023
5b8f5c7
imp(jssdk): throw error when no ContractKey found for system contract
Leechael Oct 3, 2023
18dca0b
imp(jssdk): check ApiPromise is ready or not when invoke OnChainRegis…
Leechael Oct 8, 2023
83b8aca
imp(jssdk): auto-deposit for contract instantiate
Leechael Oct 8, 2023
d7db14f
fix(jssdk): the PinkContract query failed
Leechael Oct 9, 2023
cef7239
chore(jssdk): bump beta version to 0.5.3-nightly-20231009
Leechael Oct 9, 2023
b4ef548
imp(jssdk): set deposit for gas estimate
Leechael Oct 10, 2023
dc069fb
chore(jssdk): bump beta version to 0.5.3-nightly-20231011
Leechael Oct 10, 2023
fe48830
feat(jssdk): added associated LoggerContract accessor to OnChainRegistry
Leechael Oct 11, 2023
4cb74f3
imp(jssdk): throw error when BlueprintPromise estimation failed
Leechael Oct 11, 2023
b1bc390
imp(jssdk): the OnChainRegistry.loggerContract returns void if no exi…
Leechael Oct 11, 2023
70d2ee8
imp(jssdk): grabbing related logs in PinkBlueprintSubmittableResult.w…
Leechael Oct 11, 2023
0de0526
imp(jssdk): attempt to fetch log and detect if any error returns on t…
Leechael Oct 11, 2023
694f2dc
chore(jssdk): bump beta version to 0.5.3-nightly-20231011-1
Leechael Oct 11, 2023
7141723
fix(jssdk): PinkBlueprintSubmittablePromise throws error on instantia…
Leechael Oct 11, 2023
9f16a65
chore(jssdk): bump beta version to 0.5.3-nightly-20231012
Leechael Oct 11, 2023
cdf0596
imp(jssdk): use matched keyword for instantiate failed to Exception
Leechael Oct 12, 2023
d64c028
fix(jssdk): no magic multiplier, fix the gas calculate algorithm
Leechael Oct 12, 2023
ed51b94
dependencies(jssdk): concurrently & shx
Leechael Oct 12, 2023
ce00996
build(jssdk): use concurrently improve the build speed a bit
Leechael Oct 12, 2023
5924ed4
chore(jssdk): bump beta version to 0.5.3-nightly-20231013
Leechael Oct 12, 2023
4e82f9c
imp(jssdk): use BN_MAX_SUPPLY for deposit settings in gas estimation
Leechael Oct 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions frontend/packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@phala/sdk",
"version": "0.5.2",
"version": "0.5.3-nightly-20231013",
"description": "Phala Phat Contract JS SDK",
"license": "Apache-2.0",
"homepage": "https://github.com/Phala-Network/js-sdk/tree/main/packages/sdk#readme",
Expand All @@ -27,14 +27,11 @@
"dist/*"
],
"scripts": {
"build": "npm run build:node && npm run build:browser",
"prebuild": "shx rm -rf dist",
"build": "concurrently -m 2 'yarn:build:*'",
"build:node": "tsup --config tsup.node.ts",
"build:browser": "tsup --config tsup.browser.ts",
"generate:defs": "ts-node --skip-project ./node_modules/.bin/polkadot-types-from-defs --package @phala/sdk --input ./src/interfaces --endpoint ./edgeware.json",
"generate:meta": "ts-node --skip-project ./node_modules/.bin/polkadot-types-from-chain --package @phala/sdk --endpoint ./edgeware.json --output ./src/interfaces",
"build:proto": "scripts/build_proto.sh",
"dev": "tsup --watch",
"auto-publish": "npm publish --access public",
"generate:proto": "scripts/build_proto.sh",
"pretest": "tsc --noEmit",
"prettier": "prettier '*.{js,jsolintn,md}' '{src,tests,docs}/**/*.{ts,tsx,md,mdx}' --write",
"prettier:ci": "prettier '*.{js,json,md}' '{src,tests,docs}/**/*.{ts,tsx,md,mdx}' --list-different",
Expand Down Expand Up @@ -64,6 +61,7 @@
"@typescript-eslint/parser": "^6.6.0",
"@vitest/coverage-v8": "^0.34.4",
"@vitest/ui": "^0.34.4",
"concurrently": "^8.2.1",
"esbuild": "^0.18.11",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
Expand All @@ -75,6 +73,7 @@
"eslint-plugin-vitest": "^0.3.1",
"prettier": "^3.0.3",
"protobufjs-cli": "^1.1.1",
"shx": "^0.3.4",
"ts-node": "^10.9.1",
"tsup": "^7.1.0",
"typescript": "^5.1.6",
Expand Down
15 changes: 14 additions & 1 deletion frontend/packages/sdk/src/OnChainRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BN } from '@polkadot/util'
import { waitReady } from '@polkadot/wasm-crypto'
import systemAbi from './abis/system.json'
import { PinkContractPromise } from './contracts/PinkContract'
import { PinkLoggerContractPromise } from './contracts/PinkLoggerContract'
import { type CertificateData, signCertificate } from './pruntime/certificate'
import createPruntimeClient from './pruntime/createPruntimeClient'
import { pruntime_rpc } from './pruntime/proto'
Expand Down Expand Up @@ -53,6 +54,7 @@ export class OnChainRegistry {
#phactory: pruntime_rpc.PhactoryAPI | undefined
#systemContract: PinkContractPromise | undefined
#cert: CertificateData | undefined
#loggerContract: PinkLoggerContractPromise | undefined

constructor(api: ApiPromise) {
this.api = api
Expand Down Expand Up @@ -98,7 +100,8 @@ export class OnChainRegistry {
static async create(api: ApiPromise, options?: CreateOptions) {
options = { autoConnect: true, ...(options || {}) }
const instance = new OnChainRegistry(api)
await waitReady()
// We should ensure the wasm & api has been initialized here.
await Promise.all([waitReady(), api.isReady])
if (options.autoConnect) {
await instance.connect(
options.clusterId,
Expand Down Expand Up @@ -228,6 +231,9 @@ export class OnChainRegistry {
const systemContractKey = await this.getContractKey(systemContractId)
if (systemContractKey) {
this.#systemContract = new PinkContractPromise(this.api, this, systemAbi, systemContractId, systemContractKey)
this.#loggerContract = await PinkLoggerContractPromise.create(this.api, this, this.#systemContract)
} else {
throw new Error(`System contract not found: ${systemContractId}`)
}
}
}
Expand Down Expand Up @@ -273,4 +279,11 @@ export class OnChainRegistry {
transferToCluster(address: string | AccountId, amount: number | string | BN) {
return this.api.tx.phalaPhatContracts.transferToCluster(amount, this.clusterId, address)
}

get loggerContract() {
if (this.#loggerContract) {
return this.#loggerContract
}
console.warn('Logger contract not found, you might not connect to a health cluster.')
}
}
197 changes: 149 additions & 48 deletions frontend/packages/sdk/src/contracts/PinkBlueprint.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import type { ApiPromise } from '@polkadot/api'
import { SubmittableResult, toPromiseMethod } from '@polkadot/api'
import { ApiBase } from '@polkadot/api/base'
import type { SubmittableExtrinsic } from '@polkadot/api/submittable/types'
import type { ApiTypes, DecorateMethod } from '@polkadot/api/types'
import type { ApiTypes, DecorateMethod, Signer as InjectedSigner } from '@polkadot/api/types'
import { Abi } from '@polkadot/api-contract/Abi'
import type { ContractCallResult, MessageMeta } from '@polkadot/api-contract/base/types'
import { createBluePrintTx, withMeta } from '@polkadot/api-contract/base/util'
import type { AbiConstructor, AbiMessage, BlueprintOptions, ContractCallOutcome } from '@polkadot/api-contract/types'
import type { AbiConstructor, BlueprintOptions, ContractCallOutcome } from '@polkadot/api-contract/types'
import { type Option } from '@polkadot/types'
import type { AccountId, ContractInstantiateResult, Hash } from '@polkadot/types/interfaces'
import type { ISubmittableResult } from '@polkadot/types/types'
import type { IKeyringPair, ISubmittableResult } from '@polkadot/types/types'
import { BN, BN_ZERO, hexAddPrefix, hexToU8a, isUndefined } from '@polkadot/util'
import { sr25519Agree, sr25519KeypairFromSeed } from '@polkadot/wasm-crypto'
import { from } from 'rxjs'
import type { OnChainRegistry } from '../OnChainRegistry'
import { phalaTypes } from '../options'
import type { CertificateData } from '../pruntime/certificate'
import { InkQueryInstantiate } from '../pruntime/coders'
import { pinkQuery } from '../pruntime/pinkQuery'
import type { AbiLike, InkQueryError, InkResponse } from '../types'
import type { AbiLike, FrameSystemAccountInfo, InkQueryError, InkResponse } from '../types'
import assert from '../utils/assert'
import { BN_MAX_SUPPLY } from '../utils/constants'
import { randomHex } from '../utils/hex'
import signAndSend from '../utils/signAndSend'
import { PinkContractPromise } from './PinkContract'

export interface PinkContractInstantiateCallOutcome extends ContractCallOutcome {
Expand Down Expand Up @@ -54,29 +57,27 @@ export interface PinkBlueprintOptions extends BlueprintOptions {
}

export interface PinkBlueprintDeploy<ApiType extends ApiTypes> extends MessageMeta {
(options: BlueprintOptions, ...params: unknown[]): SubmittableExtrinsic<ApiType, PinkBlueprintSubmittableResult>
(options: PinkBlueprintOptions, ...params: unknown[]): SubmittableExtrinsic<ApiType, PinkBlueprintSubmittableResult>
}

export interface PinkMapConstructorExec<ApiType extends ApiTypes> {
[message: string]: PinkBlueprintDeploy<ApiType>
}

function createQuery(
meta: AbiMessage,
fn: (
origin: string | AccountId | Uint8Array,
options: PinkInstantiateQueryOptions,
params: unknown[]
) => ContractCallResult<'promise', PinkContractInstantiateCallOutcome>
): ContractInkQuery<'promise'> {
return withMeta(
meta,
(
origin: string | AccountId | Uint8Array,
options: PinkInstantiateQueryOptions,
...params: unknown[]
): ContractCallResult<'promise', PinkContractInstantiateCallOutcome> => fn(origin, options, params)
)
interface SendOptions {
cert?: CertificateData
}

export type PinkBlueprintSendOptions =
| (PinkBlueprintOptions & SendOptions & { address: string | AccountId; signer: InjectedSigner })
| (PinkBlueprintOptions & SendOptions & { pair: IKeyringPair })

export interface PinkBlueprintSend<TParams extends Array<any> = any[]> extends MessageMeta {
(options: PinkBlueprintSendOptions, ...params: TParams): Promise<PinkBlueprintSubmittableResult>
}

interface MapBlueprintSend {
[message: string]: PinkBlueprintSend
}

export class PinkBlueprintSubmittableResult extends SubmittableResult {
Expand Down Expand Up @@ -113,9 +114,22 @@ export class PinkBlueprintSubmittableResult extends SubmittableResult {
if (!contractId) {
throw new Error('Failed to find contract ID in events, maybe instantiate failed.')
}
const logger = this.registry.loggerContract

const t0 = new Date().getTime()
while (true) {
if (logger) {
const { records } = await logger.tail(10, { contract: contractId })
if (
records.length > 0 &&
records[0].type === 'Log' &&
records[0].execMode === 'transaction' &&
records[0].message.indexOf('instantiate failed') !== -1
) {
throw new Error(records[0].message)
Leechael marked this conversation as resolved.
Show resolved Hide resolved
}
}

const result1 = (await this.registry.api.query.phalaPhatContracts.clusterContracts(
this.registry.clusterId
)) as unknown as Text[]
Expand Down Expand Up @@ -191,12 +205,22 @@ export class PinkBlueprintPromise {
this._decorateMethod = toPromiseMethod
this.phatRegistry = phatRegistry

this.codeHash = this.api.registry.createType('Hash', codeHash)

this.abi.constructors.forEach((c): void => {
if (isUndefined(this.#tx[c.method])) {
this.#tx[c.method] = createBluePrintTx(c, (o, p) => this.#deploy(c, o, p)) as PinkBlueprintDeploy<'promise'>
this.#query[c.method] = createQuery(c, (f, o, p) => this.#estimateGas(c, o, p).send(f))
this.codeHash = phalaTypes.createType('Hash', codeHash)

this.abi.constructors.forEach((meta): void => {
if (isUndefined(this.#tx[meta.method])) {
this.#tx[meta.method] = createBluePrintTx(meta, (o, p) =>
this.#deploy(meta, o, p)
) as PinkBlueprintDeploy<'promise'>
this.#query[meta.method] = withMeta(
meta,
(
origin: string | AccountId | Uint8Array,
options: PinkInstantiateQueryOptions,
...params: unknown[]
): ContractCallResult<'promise', PinkContractInstantiateCallOutcome> =>
this.#estimateGas(meta, options, params).send(origin)
)
}
})
}
Expand All @@ -209,6 +233,23 @@ export class PinkBlueprintPromise {
return this.#query
}

public get send() {
return new Proxy(
{},
{
get: (_target, prop, _receiver) => {
const meta = this.abi.constructors.filter((i) => i.method === prop)
if (!meta || !meta.length) {
throw new Error('Method not found')
}
return withMeta(meta[0], (options: PinkBlueprintSendOptions, ...arags: unknown[]) => {
return this.#send(prop as string, options, ...arags)
})
},
}
) as MapBlueprintSend
}

#deploy = (
constructorOrId: AbiConstructor | string | number,
{ gasLimit = BN_ZERO, storageDepositLimit = null, value = BN_ZERO, deposit = BN_ZERO, salt }: PinkBlueprintOptions,
Expand Down Expand Up @@ -247,8 +288,6 @@ export class PinkBlueprintPromise {
options: PinkInstantiateQueryOptions,
params: unknown[]
) => {
const api = this.api as ApiPromise

// 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)))
Expand All @@ -271,37 +310,99 @@ export class PinkBlueprintPromise {
'The associated System Contract was not set up for You OnChainRegistry, causing the estimate gas to fail.'
)
}

const salt = options.salt || randomHex(4)
const payload = api.createType('InkQuery', {
head: {
nonce: hexAddPrefix(randomHex(32)),
id: this.phatRegistry.systemContract?.address,
},
data: {
InkInstantiate: {
codeHash: this.abi.info.source.wasmHash,
salt,
instantiateData: this.abi.findConstructor(constructorOrId).toU8a(params),
deposit: options.deposit || 0,
transfer: options.transfer || 0,
},
},
})
const payload = InkQueryInstantiate(
this.phatRegistry.systemContract.address,
this.abi.info.source.wasmHash,
this.abi.findConstructor(constructorOrId).toU8a(params),
salt,
options.deposit,
options.transfer
)
const rawResponse = await pinkQuery(this.phatRegistry.phactory, pk, queryAgreementKey, payload.toHex(), cert)
const response = api.createType<InkResponse>('InkResponse', rawResponse)
const response = phalaTypes.createType<InkResponse>('InkResponse', rawResponse)
if (response.result.isErr) {
return api.createType<InkQueryError>('InkQueryError', response.result.asErr.toHex())
return phalaTypes.createType<InkQueryError>('InkQueryError', response.result.asErr.toHex())
}
const result = api.createType<ContractInstantiateResult>(
const result = phalaTypes.createType<ContractInstantiateResult>(
'ContractInstantiateResult',
response.result.asOk.asInkMessageReturn.toHex()
)
;(result as PinkContractInstantiateResult).salt = salt

if (result.result.isErr) {
const err = result.result.asErr
if (err.isModule && err.asModule.index.toNumber() === 4) {
const contractError = phalaTypes.createType('ContractError', result.result.asErr.asModule.error)
throw new Error(`Estimation failed: ${contractError.toHuman()}`)
}
throw new Error('Estimation failed: ' + JSON.stringify(result.result.asErr.toHuman()))
}

return result
}

return {
send: this._decorateMethod((origin: string | AccountId | Uint8Array) => from(inkQueryInternal(origin))),
}
}

async #send(constructorOrId: string, options: PinkBlueprintSendOptions, ...args: unknown[]) {
const { cert: userCert, ...rest } = options
const txOptions: PinkBlueprintOptions = {
gasLimit: options.gasLimit,
value: options.value,
storageDepositLimit: options.storageDepositLimit,
}

const tx = this.#tx[constructorOrId]
if (!tx) {
throw new Error(`Constructor not found: ${constructorOrId}`)
}

const address = 'signer' in rest ? rest.address : rest.pair.address
const cert = userCert || (await this.phatRegistry.getAnonymousCert())
const estimate = this.#query[constructorOrId]
if (!estimate) {
throw new Error(`Constructor not found: ${constructorOrId}`)
}

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

const [clusterBalance, onchainBalance, { gasRequired, storageDeposit }] = await Promise.all([
this.phatRegistry.getClusterBalance(address),
this.api.query.system.account<FrameSystemAccountInfo>(address),
// We estimating the gas & storage deposit cost with deposit propose.
estimate(cert.address, { cert, deposit: BN_MAX_SUPPLY }, ...args),
])

// 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.
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()}`)
}
txOptions.deposit = deposit
}

// gasLimit is required, so we set it to the estimated value if not provided.
if (!txOptions.gasLimit) {
txOptions.gasLimit = gasLimit
}

if ('signer' in rest) {
return await signAndSend(tx(txOptions, ...args), rest.address, rest.signer)
} else {
return await signAndSend(tx(txOptions, ...args), rest.pair)
}
}
}
Loading