From e647c5c4da65548c8633d932ee02a09306059931 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Mon, 9 Sep 2024 15:39:58 +0200 Subject: [PATCH 1/4] feat: add transaction_v1 group of functions --- packages/core/src/rpc/rpc-spec/index.ts | 4 +- .../core/src/rpc/rpc-spec/transaction_v1.ts | 33 ++++++++++ packages/e2e/src/helper.ts | 3 +- packages/e2e/src/rpc-spec.test.ts | 65 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/rpc/rpc-spec/transaction_v1.ts create mode 100644 packages/e2e/src/rpc-spec.test.ts diff --git a/packages/core/src/rpc/rpc-spec/index.ts b/packages/core/src/rpc/rpc-spec/index.ts index 69c04eb5..b3860d29 100644 --- a/packages/core/src/rpc/rpc-spec/index.ts +++ b/packages/core/src/rpc/rpc-spec/index.ts @@ -1,9 +1,11 @@ import * as ChainHeadV1RPC from './chainHead_v1.js' +import * as TransactionV1RPC from './transaction_v1.js' -export { ChainHeadV1RPC } +export { ChainHeadV1RPC, TransactionV1RPC } const handlers = { ...ChainHeadV1RPC, + ...TransactionV1RPC, } export default handlers diff --git a/packages/core/src/rpc/rpc-spec/transaction_v1.ts b/packages/core/src/rpc/rpc-spec/transaction_v1.ts new file mode 100644 index 00000000..81926996 --- /dev/null +++ b/packages/core/src/rpc/rpc-spec/transaction_v1.ts @@ -0,0 +1,33 @@ +import { Handler } from '../shared.js' +import { HexString } from '@polkadot/util/types' +import { defaultLogger } from '@acala-network/chopsticks-core' + +const logger = defaultLogger.child({ name: 'rpc-transaction_v1' }) +const randomId = () => Math.random().toString(36).substring(2) + +/** + * Submit the extrinsic to the transaction pool + * + * @param context + * @param params - [`extrinsic`] + * + * @return operation id + */ +export const transaction_v1_broadcast: Handler<[HexString], string | null> = async (context, [extrinsic]) => { + await context.chain.submitExtrinsic(extrinsic).catch((err) => { + // As per the spec, the invalid transaction errors should be ignored. + logger.warn('Submit extrinsic failed', err) + }) + + return randomId() +} + +/** + * Stop broadcasting the transaction to other nodes. + * + */ +export const transaction_v1_stop: Handler<[string], null> = async (_context, [_operationId]) => { + // Chopsticks doesn't have any process to broadcast the transaction through P2P + // so stopping doesn't have any effect. + return null +} diff --git a/packages/e2e/src/helper.ts b/packages/e2e/src/helper.ts index 0108eec9..3ddf8e4e 100644 --- a/packages/e2e/src/helper.ts +++ b/packages/e2e/src/helper.ts @@ -201,6 +201,7 @@ export const setupPolkadotApi = async (option: SetupOption) => { chain: null as unknown as Blockchain, substrateClient: null as unknown as SubstrateClient, observableClient: null as unknown as ObservableClient, + ws: null as unknown as WsProvider, } beforeAll(async () => { @@ -212,7 +213,7 @@ export const setupPolkadotApi = async (option: SetupOption) => { beforeEach(async () => { const res = await setup() - ws = res.ws + ws = result.ws = res.ws chain = result.chain = res.chain result.substrateClient = res.substrateClient result.observableClient = res.observableClient diff --git a/packages/e2e/src/rpc-spec.test.ts b/packages/e2e/src/rpc-spec.test.ts new file mode 100644 index 00000000..e53e1904 --- /dev/null +++ b/packages/e2e/src/rpc-spec.test.ts @@ -0,0 +1,65 @@ +import { ApiPromise } from '@polkadot/api' +import { describe, expect, it } from 'vitest' +import { dev, env, observe, setupPolkadotApi, testingPairs } from './helper.js' + +const testApi = await setupPolkadotApi(env.acalaV15) + +const { alice, bob } = testingPairs() + +describe('transaction_v1', async () => { + it('sends and executes transactions', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const api = await prepareChainForTx() + + const tx = await api.tx.balances.transferKeepAlive(bob.address, 100n).signAsync(alice) + const { nextValue, subscription } = observe(chainHead.trackTx$(tx.toHex())) + const resultPromise = nextValue() + const broadcast = testApi.observableClient.broadcastTx$(tx.toHex()).subscribe() + + // We don't have a confirmation of when the transaction has been broadcasted through the network + // it just continues to get broadcasted through the nodes until we unsubscribe from it. + // In this case, where there's only one node, a couple of blocks should be enough. + await dev.newBlock() + const hash = await dev.newBlock() + + expect(await resultPromise).toMatchObject({ + hash, + found: { + type: true, + }, + }) + + broadcast.unsubscribe() + subscription.unsubscribe() + chainHead.unfollow() + }) +}) + +const UPGRADED = 0x80000000_00000000_00000000_00000000n +const INITIAL_ACCOUNT_VALUE = 100_000_000_000_000n +async function prepareChainForTx() { + const api = await ApiPromise.create({ + provider: testApi.ws, + noInitWarn: true, + }) + await api.isReady + await dev.setStorage({ + System: { + Account: [ + [ + [alice.address], + { + data: { free: INITIAL_ACCOUNT_VALUE, flags: UPGRADED }, + }, + ], + [[bob.address], { data: { free: INITIAL_ACCOUNT_VALUE, flags: UPGRADED } }], + ], + }, + Sudo: { + Key: alice.address, + }, + }) + + return api +} From 308da253ce0da5ccce43ddafb1a14e5f34ab93ef Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Mon, 9 Sep 2024 16:45:56 +0200 Subject: [PATCH 2/4] replace new block for awaiting a set amount of time. --- packages/core/src/rpc/rpc-spec/transaction_v1.ts | 2 +- packages/e2e/src/rpc-spec.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/rpc/rpc-spec/transaction_v1.ts b/packages/core/src/rpc/rpc-spec/transaction_v1.ts index 81926996..22b2549d 100644 --- a/packages/core/src/rpc/rpc-spec/transaction_v1.ts +++ b/packages/core/src/rpc/rpc-spec/transaction_v1.ts @@ -1,6 +1,6 @@ import { Handler } from '../shared.js' import { HexString } from '@polkadot/util/types' -import { defaultLogger } from '@acala-network/chopsticks-core' +import { defaultLogger } from '../../logger.js' const logger = defaultLogger.child({ name: 'rpc-transaction_v1' }) const randomId = () => Math.random().toString(36).substring(2) diff --git a/packages/e2e/src/rpc-spec.test.ts b/packages/e2e/src/rpc-spec.test.ts index e53e1904..5d5978bb 100644 --- a/packages/e2e/src/rpc-spec.test.ts +++ b/packages/e2e/src/rpc-spec.test.ts @@ -19,8 +19,8 @@ describe('transaction_v1', async () => { // We don't have a confirmation of when the transaction has been broadcasted through the network // it just continues to get broadcasted through the nodes until we unsubscribe from it. - // In this case, where there's only one node, a couple of blocks should be enough. - await dev.newBlock() + // In this case, where there's only one node, waiting for 300ms should be enough. + await new Promise((resolve) => setTimeout(resolve, 300)) const hash = await dev.newBlock() expect(await resultPromise).toMatchObject({ From 9c6189d85fb289ec0c122b3605c6d486223a28ec Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Tue, 10 Sep 2024 09:40:59 +0200 Subject: [PATCH 3/4] change upgraded flag for providers --- packages/e2e/src/rpc-spec.test.ts | 33 +++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/e2e/src/rpc-spec.test.ts b/packages/e2e/src/rpc-spec.test.ts index 5d5978bb..f2b7588f 100644 --- a/packages/e2e/src/rpc-spec.test.ts +++ b/packages/e2e/src/rpc-spec.test.ts @@ -1,6 +1,8 @@ import { ApiPromise } from '@polkadot/api' +import { RuntimeContext } from '@polkadot-api/observable-client' import { describe, expect, it } from 'vitest' import { dev, env, observe, setupPolkadotApi, testingPairs } from './helper.js' +import { firstValueFrom } from 'rxjs' const testApi = await setupPolkadotApi(env.acalaV15) @@ -12,15 +14,16 @@ describe('transaction_v1', async () => { const api = await prepareChainForTx() - const tx = await api.tx.balances.transferKeepAlive(bob.address, 100n).signAsync(alice) + const TRANSFERRED_VALUE = 100n + const tx = await api.tx.balances.transferKeepAlive(bob.address, TRANSFERRED_VALUE).signAsync(alice) const { nextValue, subscription } = observe(chainHead.trackTx$(tx.toHex())) const resultPromise = nextValue() const broadcast = testApi.observableClient.broadcastTx$(tx.toHex()).subscribe() // We don't have a confirmation of when the transaction has been broadcasted through the network // it just continues to get broadcasted through the nodes until we unsubscribe from it. - // In this case, where there's only one node, waiting for 300ms should be enough. - await new Promise((resolve) => setTimeout(resolve, 300)) + // In this case, where there's only one node, waiting for 500ms should be enough. + await new Promise((resolve) => setTimeout(resolve, 500)) const hash = await dev.newBlock() expect(await resultPromise).toMatchObject({ @@ -30,13 +33,24 @@ describe('transaction_v1', async () => { }, }) + const keyEncoder = (addr: string) => (ctx: RuntimeContext) => + ctx.dynamicBuilder.buildStorage('System', 'Account').enc(addr) + const resultDecoder = (data: string | null, ctx: RuntimeContext) => + data ? ctx.dynamicBuilder.buildStorage('System', 'Account').dec(data) : null + expect( + await firstValueFrom(chainHead.storage$(null, 'value', keyEncoder(bob.address), null, resultDecoder)), + ).toMatchObject({ + data: { + free: INITIAL_ACCOUNT_VALUE + TRANSFERRED_VALUE, + }, + }) + broadcast.unsubscribe() subscription.unsubscribe() chainHead.unfollow() }) }) -const UPGRADED = 0x80000000_00000000_00000000_00000000n const INITIAL_ACCOUNT_VALUE = 100_000_000_000_000n async function prepareChainForTx() { const api = await ApiPromise.create({ @@ -50,10 +64,17 @@ async function prepareChainForTx() { [ [alice.address], { - data: { free: INITIAL_ACCOUNT_VALUE, flags: UPGRADED }, + providers: 1, + data: { free: INITIAL_ACCOUNT_VALUE }, + }, + ], + [ + [bob.address], + { + providers: 1, + data: { free: INITIAL_ACCOUNT_VALUE }, }, ], - [[bob.address], { data: { free: INITIAL_ACCOUNT_VALUE, flags: UPGRADED } }], ], }, Sudo: { From 3c35a8120e7dd4cf7dc2a8af93283c0465b28e23 Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Tue, 10 Sep 2024 13:29:45 +0200 Subject: [PATCH 4/4] fix(rpc-test): await for RPC confirmation that the tx is included --- packages/e2e/src/rpc-spec.test.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/e2e/src/rpc-spec.test.ts b/packages/e2e/src/rpc-spec.test.ts index f2b7588f..3a6ab7de 100644 --- a/packages/e2e/src/rpc-spec.test.ts +++ b/packages/e2e/src/rpc-spec.test.ts @@ -18,12 +18,9 @@ describe('transaction_v1', async () => { const tx = await api.tx.balances.transferKeepAlive(bob.address, TRANSFERRED_VALUE).signAsync(alice) const { nextValue, subscription } = observe(chainHead.trackTx$(tx.toHex())) const resultPromise = nextValue() - const broadcast = testApi.observableClient.broadcastTx$(tx.toHex()).subscribe() - - // We don't have a confirmation of when the transaction has been broadcasted through the network - // it just continues to get broadcasted through the nodes until we unsubscribe from it. - // In this case, where there's only one node, waiting for 500ms should be enough. - await new Promise((resolve) => setTimeout(resolve, 500)) + await new Promise((onSuccess, onError) => + testApi.substrateClient._request('transaction_v1_broadcast', [tx.toHex()], { onSuccess, onError }), + ) const hash = await dev.newBlock() expect(await resultPromise).toMatchObject({ @@ -45,7 +42,6 @@ describe('transaction_v1', async () => { }, }) - broadcast.unsubscribe() subscription.unsubscribe() chainHead.unfollow() })