diff --git a/e2e/passkey/passkeyProxyV2.ethereum.test.ts b/e2e/passkey/passkeyProxyV2.ethereum.test.ts index d5ce1b3730..6e9b0c2464 100644 --- a/e2e/passkey/passkeyProxyV2.ethereum.test.ts +++ b/e2e/passkey/passkeyProxyV2.ethereum.test.ts @@ -1,18 +1,14 @@ import '@frequency-chain/api-augment'; import assert from 'assert'; -import { - createAndFundKeypair, - EcdsaSignature, - getBlockNumber, - getNonce, - Sr25519Signature, -} from '../scaffolding/helpers'; +import { createAndFundKeypair, EcdsaSignature, getNonce, Sr25519Signature } from '../scaffolding/helpers'; import { KeyringPair } from '@polkadot/keyring/types'; -import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; import { getUnifiedPublicKey, getUnifiedAddress } from '../scaffolding/ethereum'; import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256'; import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; +import { AccountId32 } from '@polkadot/types/interfaces'; +import { ISubmittableResult } from '@polkadot/types/types'; const fundingSource = getFundingSource(import.meta.url); describe('Passkey Pallet Proxy V2 Ethereum Tests', function () { @@ -28,11 +24,13 @@ describe('Passkey Pallet Proxy V2 Ethereum Tests', function () { }); it('should transfer via passkeys with root sr25519 key into an ethereum style account', async function () { + const startingBalance = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); const accountPKey = getUnifiedPublicKey(fundedSr25519Keys); const nonce = await getNonce(fundedSr25519Keys); + const transferAmount = 55_000_000n; const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( getUnifiedAddress(receiverKeys), - 55_000_000n + transferAmount ); const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedSr25519Keys.sign(u8aWrapBytes(passKeyPublicKey)); @@ -46,42 +44,154 @@ describe('Passkey Pallet Proxy V2 Ethereum Tests', function () { false ); const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedSr25519Keys, passkeyPayload); - await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource)); - await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); - // adding some delay before fetching the nonce to ensure it is updated - await new Promise((resolve) => setTimeout(resolve, 1000)); + try { + const { + target, + eventMap: { 'balances.Transfer': transferEvent }, + } = await passkeyProxy.fundAndSendUnsigned(fundingSource); + assert.notEqual(target, undefined, 'Target event should not be undefined'); + assert.equal( + ExtrinsicHelper.api.events.balances.Transfer.is(transferEvent), + true, + 'Transfer event should be of correct type' + ); + if (transferEvent && ExtrinsicHelper.api.events.balances.Transfer.is(transferEvent)) { + const { from, to, amount } = transferEvent.data; + assert.equal( + from.toString(), + getUnifiedAddress(fundedSr25519Keys), + 'From address should be the funded sr25519 key' + ); + assert.equal(to.toString(), getUnifiedAddress(receiverKeys), 'To address should be the receiver key'); + assert.equal(amount.toBigInt(), transferAmount, `Transfer amount should be ${transferAmount}`); + } + } catch (e: any) { + assert.fail(e); + } + + /* + * Normally these checks would be unnecessary, but we are testing the passkey pallet + * which has additional logic surrounding mapping account keys, so we want to make sure + * that the nonce and balance are updated correctly. + */ const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedSr25519Keys)).nonce.toNumber(); - assert.equal(nonce + 1, nonceAfter); - }); + assert.equal(nonce + 1, nonceAfter, 'Nonce should be incremented by 1'); - it('should transfer via passkeys with root ethereum style key into another one', async function () { - const accountPKey = getUnifiedPublicKey(fundedEthereumKeys); - console.log(`accountPKey ${u8aToHex(accountPKey)}`); - const nonce = await getNonce(fundedEthereumKeys); - const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( - getUnifiedAddress(receiverKeys), - 66_000_000n + const balanceAfter = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); + assert.equal( + balanceAfter, + startingBalance + transferAmount, + 'Receiver balance should be incremented by transfer amount' ); - const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); - // ethereum keys should not have wrapping - const accountSignature = fundedEthereumKeys.sign(passKeyPublicKey); - console.log(`accountSignature ${u8aToHex(accountSignature)}`); - const multiSignature: EcdsaSignature = { Ecdsa: u8aToHex(accountSignature) }; - const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); - const passkeyPayload = await createPasskeyPayloadV2( - multiSignature, - passKeyPrivateKey, - passKeyPublicKey, - passkeyCall, - false - ); - const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundingSource, passkeyPayload); - await assert.doesNotReject(passkeyProxy.sendUnsigned()); - await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); - // adding some delay before fetching the nonce to ensure it is updated - await new Promise((resolve) => setTimeout(resolve, 1000)); - const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).nonce.toNumber(); - assert.equal(nonce + 1, nonceAfter); + }); + + describe('successful transfer via passkeys with root ethereum-style key into a polkadot-style key', function () { + let startingReceiverBalance: bigint; + let startingSenderBalance: bigint; + let accountPKey: Uint8Array; + let nonce: number; + let target: any; + let transferEvent: any; + let feeEvent: any; + let passkeyProxy: Extrinsic< + { + accountId: AccountId32; + }, + ISubmittableResult, + [accountId: AccountId32] + >; + + const transferAmount = 66_000_000n; + + before(async function () { + startingReceiverBalance = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); + startingSenderBalance = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).data.free.toBigInt(); + accountPKey = getUnifiedPublicKey(fundedEthereumKeys); + console.log(`accountPKey ${u8aToHex(accountPKey)}`); + nonce = await getNonce(fundedEthereumKeys); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( + getUnifiedAddress(receiverKeys), + transferAmount + ); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); + // ethereum keys should not have wrapping + const accountSignature = fundedEthereumKeys.sign(passKeyPublicKey); + console.log(`accountSignature ${u8aToHex(accountSignature)}`); + const multiSignature: EcdsaSignature = { Ecdsa: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); + const passkeyPayload = await createPasskeyPayloadV2( + multiSignature, + passKeyPrivateKey, + passKeyPublicKey, + passkeyCall, + false + ); + passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundingSource, passkeyPayload); + }); + + it('should successfully execute transfer extrinsic', async function () { + await assert.doesNotReject(async () => { + ({ + target, + eventMap: { 'balances.Transfer': transferEvent, 'balances.Withdraw': feeEvent }, + } = await passkeyProxy.sendUnsigned()); + }); + }); + + it('should have received a transaction execution success event', async function () { + assert.notEqual(target, undefined, 'Target event should not be undefined'); + assert.equal( + ExtrinsicHelper.api.events.passkey.TransactionExecutionSuccess.is(target), + true, + 'Target event should be of correct type' + ); + }); + + it('should have debited & credited correct accounts for the correct amount', async function () { + if (transferEvent && ExtrinsicHelper.api.events.balances.Transfer.is(transferEvent)) { + const { from, to, amount } = transferEvent.data; + assert.equal(from.toString(), getUnifiedAddress(fundedEthereumKeys), 'From address should be the funded key'); + assert.equal(to.toString(), getUnifiedAddress(receiverKeys), 'To address should be the receiver key'); + assert.equal(amount.toBigInt(), transferAmount, `Transfer amount should be ${transferAmount}`); + } else { + assert.fail('Transfer event not found'); + } + }); + + it('should have deducted fee from the sender', async function () { + if (feeEvent && ExtrinsicHelper.api.events.balances.Withdraw.is(feeEvent)) { + const { who, amount } = feeEvent.data; + assert.equal(who.toString(), getUnifiedAddress(fundedEthereumKeys), 'Fee should be deducted from the sender'); + assert.equal(amount.toBigInt() > 0, true, 'Fee should be greater than 0'); + } else { + assert.fail('Fee event not found'); + } + }); + + /* + * Normally these checks would be unnecessary, but we are testing the passkey pallet + * which has additional logic surrounding mapping account keys, so we want to make sure + * that the nonce and balance are updated correctly. + */ + it('should have incremented nonce by 1 for sender', async function () { + const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).nonce.toNumber(); + assert.equal(nonce + 1, nonceAfter, 'Nonce should be incremented by 1'); + }); + + it('should have changed account balances correctly', async function () { + const endingReceiverBalance = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); + const endingSenderBalance = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).data.free.toBigInt(); + assert.equal( + endingReceiverBalance, + startingReceiverBalance + transferAmount, + 'Receiver balance should be incremented by transfer amount' + ); + assert.equal( + startingSenderBalance - transferAmount >= endingSenderBalance, + true, + 'Sender balance should be decremented by at least transfer amount' + ); + }); }); }); }); diff --git a/e2e/passkey/passkeyProxyV2.test.ts b/e2e/passkey/passkeyProxyV2.test.ts index d39a4fecfe..7ab5eb5d79 100644 --- a/e2e/passkey/passkeyProxyV2.test.ts +++ b/e2e/passkey/passkeyProxyV2.test.ts @@ -2,11 +2,13 @@ import '@frequency-chain/api-augment'; import assert from 'assert'; import { createAndFundKeypair, getBlockNumber, getNonce, Sr25519Signature } from '../scaffolding/helpers'; import { KeyringPair } from '@polkadot/keyring/types'; -import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; import { createPassKeyAndSignAccount, createPassKeyCallV2, createPasskeyPayloadV2 } from '../scaffolding/P256'; -import { getUnifiedPublicKey } from '../scaffolding/ethereum'; +import { getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum'; +import { AccountId32 } from '@polkadot/types/interfaces'; +import { ISubmittableResult } from '@polkadot/types/types'; const fundingSource = getFundingSource(import.meta.url); describe('Passkey Pallet Proxy V2 Tests', function () { @@ -24,7 +26,7 @@ describe('Passkey Pallet Proxy V2 Tests', function () { const nonce = await getNonce(fundedKeys); const remarksCalls = ExtrinsicHelper.api.tx.system.remark('passkey-test'); - const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, remarksCalls); @@ -37,14 +39,14 @@ describe('Passkey Pallet Proxy V2 Tests', function () { ); const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload); - await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource), /Transaction call is not expected/); }); it('should fail to transfer balance due to bad account ownership proof', async function () { const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n); - const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign('badPasskeyPublicKey'); const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); @@ -57,14 +59,14 @@ describe('Passkey Pallet Proxy V2 Tests', function () { ); const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload); - await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource), /Invalid signing address/); }); it('should fail to transfer balance due to bad passkey signature', async function () { const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n); - const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); @@ -77,36 +79,113 @@ describe('Passkey Pallet Proxy V2 Tests', function () { ); const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload); - await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource), /Custom error: 4/); }); - it('should transfer small balance from fundedKeys to receiverKeys', async function () { - const accountPKey = getUnifiedPublicKey(fundedKeys); - const nonce = await getNonce(fundedKeys); - const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( - getUnifiedPublicKey(receiverKeys), - 100_000_000n - ); - const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); - const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); - const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; - const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); - const passkeyPayload = await createPasskeyPayloadV2( - multiSignature, - passKeyPrivateKey, - passKeyPublicKey, - passkeyCall, - false - ); - const passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload); - await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource)); - await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); - const receiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys); - // adding some delay before fetching the nonce to ensure it is updated - await new Promise((resolve) => setTimeout(resolve, 2000)); - const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).nonce.toNumber(); - assert.equal(nonce + 1, nonceAfter); - assert(receiverBalance.data.free.toBigInt() > 0n); + describe('successful transfer small balance from fundedKeys to receiverKeys', function () { + let startingReceiverBalance: bigint; + let startingSenderBalance: bigint; + let accountPKey: Uint8Array; + let nonce: number; + let target: any; + let transferEvent: any; + let feeEvent: any; + let passkeyProxy: Extrinsic< + { + accountId: AccountId32; + }, + ISubmittableResult, + [accountId: AccountId32] + >; + + const transferAmount = 100_000_000n; + + before(async function () { + startingReceiverBalance = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); + startingSenderBalance = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).data.free.toBigInt(); + accountPKey = getUnifiedPublicKey(fundedKeys); + nonce = await getNonce(fundedKeys); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( + getUnifiedPublicKey(receiverKeys), + transferAmount + ); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); + const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCallV2(accountPKey, nonce, transferCalls); + const passkeyPayload = await createPasskeyPayloadV2( + multiSignature, + passKeyPrivateKey, + passKeyPublicKey, + passkeyCall, + false + ); + passkeyProxy = ExtrinsicHelper.executePassKeyProxyV2(fundedKeys, passkeyPayload); + }); + + it('should successfully execute transfer extrinsic', async function () { + await assert.doesNotReject(async () => { + ({ + target, + eventMap: { 'balances.Transfer': transferEvent, 'balances.Withdraw': feeEvent }, + } = await passkeyProxy.fundAndSendUnsigned(fundingSource)); + }); + }); + + it('should have received a transaction execution success event', async function () { + assert.notEqual(target, undefined, 'Target event should not be undefined'); + assert.equal( + ExtrinsicHelper.api.events.balances.Transfer.is(transferEvent), + true, + 'Transfer event should be of correct type' + ); + }); + + it('should have debited and credited correct accounts for the correct amount', async function () { + if (transferEvent && ExtrinsicHelper.api.events.balances.Transfer.is(transferEvent)) { + const { from, to, amount } = transferEvent.data; + assert.equal(from.toString(), getUnifiedAddress(fundedKeys), 'From address should be the funded key'); + assert.equal(to.toString(), getUnifiedAddress(receiverKeys), 'To address should be the receiver key'); + assert.equal(amount.toBigInt(), transferAmount, `Transfer amount should be ${transferAmount}`); + } else { + assert.fail('Transfer event not found'); + } + }); + + it('should have deducted the correct fee from the sender', async function () { + if (feeEvent && ExtrinsicHelper.api.events.balances.Withdraw.is(feeEvent)) { + const { who, amount } = feeEvent.data; + assert.equal(who.toString(), getUnifiedAddress(fundedKeys), 'Fee should be deducted from the sender'); + assert.equal(amount.toBigInt() > 0n, true, 'Fee should be greater than 0'); + } else { + assert.fail('Fee event not found'); + } + }); + + /* + * Normally these checks would be unnecessary, but we are testing the passkey pallet + * which has additional logic surrounding mapping account keys, so we want to make sure + * that the nonce and balance are updated correctly. + */ + it('should have incremented nonce by 1 for sender', async function () { + const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).nonce.toNumber(); + assert.equal(nonce + 1, nonceAfter, 'Nonce should be incremented by 1'); + }); + + it('should have changed account balances correctly', async function () { + const endingReceiverBalance = (await ExtrinsicHelper.getAccountInfo(receiverKeys)).data.free.toBigInt(); + const endingSenderBalance = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).data.free.toBigInt(); + assert.equal( + endingReceiverBalance, + startingReceiverBalance + transferAmount, + 'Receiver balance should be incremented by transfer amount' + ); + assert.equal( + startingSenderBalance - transferAmount >= endingSenderBalance, + true, + 'Sender balance should be decremented by at least transfer amount' + ); + }); }); }); }); diff --git a/e2e/scaffolding/extrinsicHelpers.ts b/e2e/scaffolding/extrinsicHelpers.ts index 26454538d7..e22292a79c 100644 --- a/e2e/scaffolding/extrinsicHelpers.ts +++ b/e2e/scaffolding/extrinsicHelpers.ts @@ -3,7 +3,7 @@ import { ApiPromise, ApiRx } from '@polkadot/api'; import { ApiTypes, AugmentedEvent, SubmittableExtrinsic, SignerOptions } from '@polkadot/api/types'; import { KeyringPair } from '@polkadot/keyring/types'; import { Compact, u128, u16, u32, u64, Vec, Option, Bool } from '@polkadot/types'; -import { FrameSystemAccountInfo, PalletPasskeyPasskeyPayload, SpRuntimeDispatchError } from '@polkadot/types/lookup'; +import { FrameSystemAccountInfo, SpRuntimeDispatchError } from '@polkadot/types/lookup'; import { AnyJson, AnyNumber, AnyTuple, Codec, IEvent, ISubmittableResult } from '@polkadot/types/types'; import { firstValueFrom, filter, map, pipe, tap } from 'rxjs'; import { getBlockNumber, getExistentialDeposit, getFinalizedBlockNumber, log, MultiSignatureType } from './helpers'; @@ -264,6 +264,10 @@ export class Extrinsic