diff --git a/e2e/capacity/capacityFail.test.ts b/e2e/capacity/capacityFail.test.ts index fefed7c903..d6b1a5c41c 100644 --- a/e2e/capacity/capacityFail.test.ts +++ b/e2e/capacity/capacityFail.test.ts @@ -19,6 +19,7 @@ import { assertAddNewKey, } from '../scaffolding/helpers'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const FUNDS_AMOUNT: bigint = 50n * DOLLARS; const fundingSource = getFundingSource('capacity-transactions-fail'); @@ -111,7 +112,7 @@ describe('Capacity Transaction Failures', function () { // As current owner, add a new set of control keys that do not have a balance. const newControlKeypair = createKeys('NewKeyNoBalance'); - const newPublicKey = newControlKeypair.publicKey; + const newPublicKey = getUnifiedPublicKey(newControlKeypair); const addKeyPayload: AddKeyData = await generateAddKeyPayload({ msaId: capacityProvider, newPublicKey: newPublicKey, @@ -193,7 +194,7 @@ describe('Capacity Transaction Failures', function () { // Add new key const newKeyPayload: AddKeyData = await generateAddKeyPayload({ msaId: new u64(ExtrinsicHelper.api.registry, capacityProvider), - newPublicKey: noTokensKeys.publicKey, + newPublicKey: getUnifiedPublicKey(noTokensKeys), }); const addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newKeyPayload); diff --git a/e2e/capacity/capacity_rpc.test.ts b/e2e/capacity/capacity_rpc.test.ts index acb5653dff..f83882a10c 100644 --- a/e2e/capacity/capacity_rpc.test.ts +++ b/e2e/capacity/capacity_rpc.test.ts @@ -13,6 +13,7 @@ import { } from '../scaffolding/helpers'; import { FeeDetails } from '@polkadot/types/interfaces'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const FUNDS_AMOUNT: bigint = 50n * DOLLARS; const fundingSource = getFundingSource('capacity-rpcs'); @@ -46,7 +47,7 @@ describe('Capacity RPC', function () { const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', addProviderPayload); const delegatorKeys = createKeys('delegatorKeys'); const call = ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signPayloadSr25519(delegatorKeys, addProviderData), addProviderPayload ); @@ -89,7 +90,7 @@ describe('Capacity RPC', function () { const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', addProviderPayload); const delegatorKeys = createKeys('delegatorKeys'); const insideTx = ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signPayloadSr25519(delegatorKeys, addProviderData), addProviderPayload ); diff --git a/e2e/capacity/transactions.test.ts b/e2e/capacity/transactions.test.ts index 1498e8e89b..b64cb6d507 100644 --- a/e2e/capacity/transactions.test.ts +++ b/e2e/capacity/transactions.test.ts @@ -38,6 +38,7 @@ import { } from '../scaffolding/helpers'; import { ipfsCid } from '../messages/ipfs'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const FUNDS_AMOUNT: bigint = 50n * DOLLARS; const fundingSource = getFundingSource('capacity-transactions'); @@ -92,7 +93,7 @@ describe('Capacity Transactions', function () { authorizedKeys.push(await createAndFundKeypair(fundingSource, 50_000_000n)); defaultPayload.msaId = capacityProvider; - defaultPayload.newPublicKey = authorizedKeys[0].publicKey; + defaultPayload.newPublicKey = getUnifiedPublicKey(authorizedKeys[0]); const payload = await generateAddKeyPayload(defaultPayload); const addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); diff --git a/e2e/capacity/transactionsBatch.test.ts b/e2e/capacity/transactionsBatch.test.ts index b8845411ee..6af3079a73 100644 --- a/e2e/capacity/transactionsBatch.test.ts +++ b/e2e/capacity/transactionsBatch.test.ts @@ -16,6 +16,7 @@ import { getTestHandle, } from '../scaffolding/helpers'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const FUNDS_AMOUNT: bigint = 50n * DOLLARS; const fundingSource = getFundingSource('capacity-transactions-batch'); @@ -49,7 +50,7 @@ describe('Capacity Transactions Batch', function () { const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', addProviderPayload); const delegatorKeys = createKeys('delegatorKeys'); const createSponsoredAccountWithDelegation = ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signPayloadSr25519(delegatorKeys, addProviderData), addProviderPayload ); @@ -70,7 +71,7 @@ describe('Capacity Transactions Batch', function () { }; const claimHandle = ExtrinsicHelper.api.tx.handles.claimHandle( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), claimHandleProof, claimHandlePayload ); @@ -95,7 +96,7 @@ describe('Capacity Transactions Batch', function () { const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', addProviderPayload); const delegatorKeys = createKeys('delegatorKeys'); const createSponsoredAccountWithDelegation = ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signPayloadSr25519(delegatorKeys, addProviderData), addProviderPayload ); @@ -116,7 +117,7 @@ describe('Capacity Transactions Batch', function () { }; const claimHandle = ExtrinsicHelper.api.tx.handles.claimHandle( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), calimHandleProof, claimHandlePayload ); diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index e8f62eccce..f1de04d4c0 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -51,6 +51,19 @@ export default tseslint.config( }, ], 'allow-namespace': 'off', + 'no-restricted-syntax': [ + 'error', + { + message: + 'Direct usage of keyPair.address is not allowed in this file. please use getUnifiedAddress function.', + selector: 'MemberExpression[property.name="address"]', + }, + { + message: + 'Direct usage of keyPair.publicKey is not allowed in this file. please use getUnifiedPublicKey function', + selector: 'MemberExpression[property.name="publicKey"]', + }, + ], }, } ); diff --git a/e2e/load-tests/signatureRegistry.test.ts b/e2e/load-tests/signatureRegistry.test.ts index ee2547edd2..789de5599d 100644 --- a/e2e/load-tests/signatureRegistry.test.ts +++ b/e2e/load-tests/signatureRegistry.test.ts @@ -13,7 +13,7 @@ import { KeyringPair } from '@polkadot/keyring/types'; import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { u64, Option } from '@polkadot/types'; import { getFundingSource } from '../scaffolding/funding'; -import { getUnifiedAddress } from '../scaffolding/ethereum'; +import { getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum'; interface GeneratedMsa { id: u64; @@ -154,7 +154,7 @@ async function addSigs(msaId: u64, keys: KeyringPair, blockNumber: number, nonce const defaultPayload: AddKeyData = {}; defaultPayload.msaId = msaId; - defaultPayload.newPublicKey = newKeys.publicKey; + defaultPayload.newPublicKey = getUnifiedPublicKey(newKeys); const payload = await generateAddKeyPayload(defaultPayload, 100, blockNumber); const addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); diff --git a/e2e/miscellaneous/balance.ethereum.test.ts b/e2e/miscellaneous/balance.ethereum.test.ts new file mode 100644 index 0000000000..0cd67b9ff1 --- /dev/null +++ b/e2e/miscellaneous/balance.ethereum.test.ts @@ -0,0 +1,67 @@ +import '@frequency-chain/api-augment'; +import assert from 'assert'; +import { DOLLARS, createAndFundKeypair, createKeys } from '../scaffolding/helpers'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; + +const fundingSource: KeyringPair = getFundingSource('frequency-balance-ethereum'); + +describe('Balance transfer ethereum', function () { + describe('setup', function () { + let senderSr25519Keys: KeyringPair; + let senderEthereumKeys: KeyringPair; + let ethereumKeys: KeyringPair; + let ethereumKeys2: KeyringPair; + let sr25519Keys: KeyringPair; + + before(async function () { + senderSr25519Keys = await createAndFundKeypair(fundingSource, 30n * DOLLARS); + senderEthereumKeys = await createAndFundKeypair(fundingSource, 30n * DOLLARS, undefined, undefined, 'ethereum'); + ethereumKeys = await createKeys('another-key-1', 'ethereum'); + ethereumKeys2 = await createKeys('another-key-2', 'ethereum'); + sr25519Keys = await createKeys('another-sr25519', 'sr25519'); + }); + + it('should transfer from sr25519 to ethereum style key', async function () { + const transferAmount = 10n * DOLLARS; + const extrinsic = new Extrinsic( + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(ethereumKeys), transferAmount), + senderSr25519Keys, + ExtrinsicHelper.api.events.balances.Transfer + ); + const { target } = await extrinsic.signAndSend(); + assert.notEqual(target, undefined, 'should have returned Transfer event'); + const accountInfo = await ExtrinsicHelper.getAccountInfo(ethereumKeys); + assert(accountInfo.data.free.toBigInt() >= transferAmount); + }); + + it('should transfer from sr25519 to ethereum 20 byte address', async function () { + const transferAmount = 10n * DOLLARS; + const extrinsic = new Extrinsic( + // this is using MultiAddress::Address20 type in Rust since addressRaw is 20 bytes ethereum address + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(ethereumKeys2.addressRaw, transferAmount), + senderSr25519Keys, + ExtrinsicHelper.api.events.balances.Transfer + ); + const { target } = await extrinsic.signAndSend(); + assert.notEqual(target, undefined, 'should have returned Transfer event'); + const accountInfo = await ExtrinsicHelper.getAccountInfo(ethereumKeys2); + assert(accountInfo.data.free.toBigInt() >= transferAmount); + }); + + it('should transfer from an ethereum key to sr25519 key', async function () { + const transferAmount = 10n * DOLLARS; + const extrinsic = new Extrinsic( + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(sr25519Keys), transferAmount), + senderEthereumKeys, + ExtrinsicHelper.api.events.balances.Transfer + ); + const { target } = await extrinsic.signAndSend(); + assert.notEqual(target, undefined, 'should have returned Transfer event'); + const accountInfo = await ExtrinsicHelper.getAccountInfo(sr25519Keys); + assert(accountInfo.data.free.toBigInt() >= transferAmount); + }); + }); +}); diff --git a/e2e/miscellaneous/frequency.test.ts b/e2e/miscellaneous/frequency.test.ts index d0848032dc..ce3c3b4330 100644 --- a/e2e/miscellaneous/frequency.test.ts +++ b/e2e/miscellaneous/frequency.test.ts @@ -6,7 +6,7 @@ import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; import { u8, Option } from '@polkadot/types'; import { u8aToHex } from '@polkadot/util/u8a/toHex'; -import { getUnifiedAddress } from '../scaffolding/ethereum'; +import { getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum'; const fundingSource: KeyringPair = getFundingSource('frequency-misc'); @@ -23,7 +23,7 @@ describe('Frequency', function () { it('Get events successfully', async function () { const balance_pallet = new u8(ExtrinsicHelper.api.registry, 10); const transfer_event = new u8(ExtrinsicHelper.api.registry, 2); - const dest_account = u8aToHex(keypairB.publicKey).slice(2); + const dest_account = u8aToHex(getUnifiedPublicKey(keypairB)).slice(2); const beforeBlockNumber = await getBlockNumber(); const extrinsic = new Extrinsic( @@ -66,7 +66,7 @@ describe('Frequency', function () { } // wait a little for all of the above transactions to get queued await new Promise((resolve) => setTimeout(resolve, 1000)); - const missingNonce = await ExtrinsicHelper.getMissingNonceValues(keypairB.publicKey); + const missingNonce = await ExtrinsicHelper.getMissingNonceValues(getUnifiedPublicKey(keypairB)); assert.equal(missingNonce.length, 4, 'Could not get missing nonce values'); // applying the missing nonce values to next transactions to unblock the stuck ones diff --git a/e2e/msa/keyManagement.ethereum.test.ts b/e2e/msa/keyManagement.ethereum.test.ts new file mode 100644 index 0000000000..c66dd2f996 --- /dev/null +++ b/e2e/msa/keyManagement.ethereum.test.ts @@ -0,0 +1,122 @@ +import '@frequency-chain/api-augment'; +import assert from 'assert'; +import { + createKeys, + createAndFundKeypair, + generateAddKeyPayload, + CENTS, + signPayload, + MultiSignatureType, +} from '../scaffolding/helpers'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { u64 } from '@polkadot/types'; +import { Codec } from '@polkadot/types/types'; +import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; + +const maxU64 = 18_446_744_073_709_551_615n; +const fundingSource = getFundingSource('msa-key-management-ethereum'); + +describe('MSA Key management Ethereum', function () { + describe('addPublicKeyToMsa Ethereum', function () { + let keys: KeyringPair; + let msaId: u64; + let secondaryKey: KeyringPair; + const defaultPayload: AddKeyData = {}; + let payload: AddKeyData; + let ownerSig: MultiSignatureType; + let newSig: MultiSignatureType; + let badSig: MultiSignatureType; + let addKeyData: Codec; + + before(async function () { + // Setup an MSA with one key and a secondary funded key + keys = await createAndFundKeypair(fundingSource, 5n * CENTS, undefined, undefined, 'ethereum'); + const { target } = await ExtrinsicHelper.createMsa(keys).signAndSend(); + assert.notEqual(target?.data.msaId, undefined, 'MSA Id not in expected event'); + msaId = target!.data.msaId; + + secondaryKey = await createAndFundKeypair(fundingSource, 5n * CENTS, undefined, undefined, 'ethereum'); + + // Default payload making it easier to test `addPublicKeyToMsa` + defaultPayload.msaId = msaId; + defaultPayload.newPublicKey = getUnifiedPublicKey(secondaryKey); + }); + + beforeEach(async function () { + payload = await generateAddKeyPayload(defaultPayload); + }); + + it('should fail to add public key if origin is not one of the signers of the payload (MsaOwnershipInvalidSignature) for a Ethereum key', async function () { + const badKeys: KeyringPair = createKeys(); + addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); + newSig = signPayload(secondaryKey, addKeyData); + badSig = signPayload(badKeys, addKeyData); + const op = ExtrinsicHelper.addPublicKeyToMsa(keys, badSig, newSig, payload); + await assert.rejects(op.fundAndSend(fundingSource), { + name: 'MsaOwnershipInvalidSignature', + }); + }); + + it('should fail to add public key if origin does not own MSA (NotMsaOwner) for a Ethereum key', async function () { + const newPayload = await generateAddKeyPayload({ + ...defaultPayload, + msaId: new u64(ExtrinsicHelper.api.registry, maxU64), + }); + addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload); + ownerSig = signPayload(keys, addKeyData); + newSig = signPayload(secondaryKey, addKeyData); + const op = ExtrinsicHelper.addPublicKeyToMsa(keys, ownerSig, newSig, newPayload); + await assert.rejects(op.fundAndSend(fundingSource), { + name: 'NotMsaOwner', + }); + }); + + it('should successfully add a new public key to an existing MSA & disallow duplicate signed payload submission (SignatureAlreadySubmitted) for a Ethereum key', async function () { + addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); + + ownerSig = signPayload(keys, addKeyData); + newSig = signPayload(secondaryKey, addKeyData); + const addPublicKeyOp = ExtrinsicHelper.addPublicKeyToMsa(keys, ownerSig, newSig, payload); + + const { target: publicKeyEvents } = await addPublicKeyOp.fundAndSend(fundingSource); + + assert.notEqual(publicKeyEvents, undefined, 'should have added public key'); + + await assert.rejects( + addPublicKeyOp.fundAndSend(fundingSource), + 'should reject sending the same signed payload twice' + ); + }); + + it('should fail if attempting to add the same key more than once (KeyAlreadyRegistered) for a Ethereum key', async function () { + const addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); + + const ownerSig = signPayload(keys, addKeyData); + const newSig = signPayload(secondaryKey, addKeyData); + const addPublicKeyOp = ExtrinsicHelper.addPublicKeyToMsa(keys, ownerSig, newSig, payload); + + await assert.rejects(addPublicKeyOp.fundAndSend(fundingSource), { + name: 'KeyAlreadyRegistered', + }); + }); + + it('should allow new keypair to act for/on MSA for a Ethereum key', async function () { + const thirdKey = createKeys(); + const newPayload = await generateAddKeyPayload({ + ...defaultPayload, + newPublicKey: getUnifiedPublicKey(thirdKey), + }); + addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload); + ownerSig = signPayload(secondaryKey, addKeyData); + newSig = signPayload(thirdKey, addKeyData); + const op = ExtrinsicHelper.addPublicKeyToMsa(secondaryKey, ownerSig, newSig, newPayload); + const { target: event } = await op.fundAndSend(fundingSource); + assert.notEqual(event, undefined, 'should have added public key'); + + // Cleanup + await assert.doesNotReject(ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(thirdKey)).signAndSend()); + }); + }); +}); diff --git a/e2e/msa/msaKeyManagement.test.ts b/e2e/msa/msaKeyManagement.test.ts index 672257346f..36a3f836c8 100644 --- a/e2e/msa/msaKeyManagement.test.ts +++ b/e2e/msa/msaKeyManagement.test.ts @@ -14,6 +14,7 @@ import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { u64 } from '@polkadot/types'; import { Codec } from '@polkadot/types/types'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const maxU64 = 18_446_744_073_709_551_615n; const fundingSource = getFundingSource('msa-key-management'); @@ -41,7 +42,7 @@ describe('MSA Key management', function () { // Default payload making it easier to test `addPublicKeyToMsa` defaultPayload.msaId = msaId; - defaultPayload.newPublicKey = secondaryKey.publicKey; + defaultPayload.newPublicKey = getUnifiedPublicKey(secondaryKey); }); beforeEach(async function () { @@ -157,7 +158,7 @@ describe('MSA Key management', function () { const thirdKey = createKeys(); const newPayload = await generateAddKeyPayload({ ...defaultPayload, - newPublicKey: thirdKey.publicKey, + newPublicKey: getUnifiedPublicKey(thirdKey), }); addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload); ownerSig = signPayloadSr25519(secondaryKey, addKeyData); @@ -167,7 +168,7 @@ describe('MSA Key management', function () { assert.notEqual(event, undefined, 'should have added public key'); // Cleanup - await assert.doesNotReject(ExtrinsicHelper.deletePublicKey(keys, thirdKey.publicKey).signAndSend()); + await assert.doesNotReject(ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(thirdKey)).signAndSend()); }); }); @@ -200,7 +201,7 @@ describe('MSA Key management', function () { const payload = await generateAddKeyPayload({ msaId, - newPublicKey: secondaryKey.publicKey, + newPublicKey: getUnifiedPublicKey(secondaryKey), }); const payloadData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload); const ownerSig = signPayloadSr25519(keys, payloadData); @@ -219,7 +220,7 @@ describe('MSA Key management', function () { }); it('should fail to delete public key for self', async function () { - const op = ExtrinsicHelper.deletePublicKey(keys, keys.publicKey); + const op = ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(keys)); await assert.rejects(op.signAndSend('current'), { name: 'RpcError', message: /Custom error: 4/, @@ -229,7 +230,7 @@ describe('MSA Key management', function () { it("should fail to delete key if not authorized for key's MSA", async function () { const [providerKeys] = await createProviderKeysAndId(fundingSource); - const op = ExtrinsicHelper.deletePublicKey(providerKeys, keys.publicKey); + const op = ExtrinsicHelper.deletePublicKey(providerKeys, getUnifiedPublicKey(keys)); await assert.rejects(op.signAndSend('current'), { name: 'RpcError', message: /Custom error: 5/, @@ -238,7 +239,7 @@ describe('MSA Key management', function () { it("should test for 'NoKeyExists' error", async function () { const key = createKeys('nothing key'); - const op = ExtrinsicHelper.deletePublicKey(keys, key.publicKey); + const op = ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(key)); await assert.rejects(op.signAndSend('current'), { name: 'RpcError', message: /Custom error: 1/, @@ -246,7 +247,7 @@ describe('MSA Key management', function () { }); it('should delete secondary key', async function () { - const op = ExtrinsicHelper.deletePublicKey(keys, secondaryKey.publicKey); + const op = ExtrinsicHelper.deletePublicKey(keys, getUnifiedPublicKey(secondaryKey)); const { target: event } = await op.signAndSend(); assert.notEqual(event, undefined, 'should have returned PublicKeyDeleted event'); }); diff --git a/e2e/passkey/passkeyProxy.ethereum.test.ts b/e2e/passkey/passkeyProxy.ethereum.test.ts index 2aa71b72e6..03daef3f2b 100644 --- a/e2e/passkey/passkeyProxy.ethereum.test.ts +++ b/e2e/passkey/passkeyProxy.ethereum.test.ts @@ -4,14 +4,13 @@ import { createAndFundKeypair, EcdsaSignature, getBlockNumber, - getNextEpochBlock, getNonce, Sr25519Signature, } from '../scaffolding/helpers'; import { KeyringPair } from '@polkadot/keyring/types'; import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; -import { getConvertedEthereumPublicKey, getUnifiedAddress } from '../scaffolding/ethereum'; +import { getUnifiedPublicKey, getUnifiedAddress } from '../scaffolding/ethereum'; import { createPassKeyAndSignAccount, createPassKeyCall, createPasskeyPayload } from '../scaffolding/P256'; import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; const fundingSource = getFundingSource('passkey-proxy-ethereum'); @@ -25,12 +24,12 @@ describe('Passkey Pallet Ethereum Tests', function () { before(async function () { fundedSr25519Keys = await createAndFundKeypair(fundingSource, 300_000_000n); fundedEthereumKeys = await createAndFundKeypair(fundingSource, 300_000_000n, undefined, undefined, 'ethereum'); - receiverKeys = await createAndFundKeypair(fundingSource, undefined, undefined, undefined, 'ethereum'); + receiverKeys = await createAndFundKeypair(fundingSource); }); it('should transfer via passkeys with root sr25519 key into an ethereum style account', async function () { const initialReceiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys); - const accountPKey = fundedSr25519Keys.publicKey; + const accountPKey = getUnifiedPublicKey(fundedSr25519Keys); const nonce = await getNonce(fundedSr25519Keys); const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( getUnifiedAddress(receiverKeys), @@ -51,7 +50,8 @@ describe('Passkey Pallet Ethereum Tests', function () { }); it('should transfer via passkeys with root ethereum style key into another one', async function () { - const accountPKey = getConvertedEthereumPublicKey(fundedEthereumKeys); + const initialReceiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys); + const accountPKey = getUnifiedPublicKey(fundedEthereumKeys); console.log(`accountPKey ${u8aToHex(accountPKey)}`); const nonce = await getNonce(fundedEthereumKeys); const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( diff --git a/e2e/passkey/passkeyProxy.test.ts b/e2e/passkey/passkeyProxy.test.ts index 4b59e4e807..b467018aef 100644 --- a/e2e/passkey/passkeyProxy.test.ts +++ b/e2e/passkey/passkeyProxy.test.ts @@ -12,6 +12,7 @@ import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; import { createPassKeyAndSignAccount, createPassKeyCall, createPasskeyPayload } from '../scaffolding/P256'; +import { getUnifiedPublicKey } from '../scaffolding/ethereum'; const fundingSource = getFundingSource('passkey-proxy'); describe('Passkey Pallet Tests', function () { @@ -25,7 +26,7 @@ describe('Passkey Pallet Tests', function () { }); it('should fail due to unsupported call', async function () { - const accountPKey = fundedKeys.publicKey; + const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); const remarksCalls = ExtrinsicHelper.api.tx.system.remark('passkey-test'); @@ -40,9 +41,9 @@ describe('Passkey Pallet Tests', function () { }); it('should fail to transfer balance due to bad account ownership proof', async function () { - const accountPKey = fundedKeys.publicKey; + const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); - const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 0n); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n); const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign('badPasskeyPublicKey'); const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; @@ -54,9 +55,9 @@ describe('Passkey Pallet Tests', function () { }); it('should fail to transfer balance due to bad passkey signature', async function () { - const accountPKey = fundedKeys.publicKey; + const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); - const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 0n); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedPublicKey(receiverKeys), 0n); const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; @@ -68,9 +69,12 @@ describe('Passkey Pallet Tests', function () { }); it('should transfer small balance from fundedKeys to receiverKeys', async function () { - const accountPKey = fundedKeys.publicKey; + const accountPKey = getUnifiedPublicKey(fundedKeys); const nonce = await getNonce(fundedKeys); - const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 100_000_000n); + 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) }; diff --git a/e2e/scaffolding/P256.ts b/e2e/scaffolding/P256.ts index 25aa16ce69..3d3fc3cbd6 100644 --- a/e2e/scaffolding/P256.ts +++ b/e2e/scaffolding/P256.ts @@ -1,5 +1,5 @@ import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { base64UrlToUint8Array, Sr25519Signature, Ed25519Signature, EcdsaSignature } from './helpers'; +import { base64UrlToUint8Array, MultiSignatureType } from './helpers'; import { secp256r1 } from '@noble/curves/p256'; import { ISubmittableResult } from '@polkadot/types/types'; import { u8aWrapBytes } from '@polkadot/util'; @@ -16,7 +16,7 @@ export function createPassKeyAndSignAccount(accountPKey: Uint8Array) { export async function createPassKeyCall( accountPKey: Uint8Array, nonce: number, - accountSignature: Sr25519Signature | Ed25519Signature | EcdsaSignature, + accountSignature: MultiSignatureType, call: SubmittableExtrinsic<'rxjs', ISubmittableResult> ) { const ext_call_type = ExtrinsicHelper.api.registry.createType('Call', call); diff --git a/e2e/scaffolding/ethereum.ts b/e2e/scaffolding/ethereum.ts index 69eecf7638..4137452ac2 100644 --- a/e2e/scaffolding/ethereum.ts +++ b/e2e/scaffolding/ethereum.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-restricted-syntax */ import { KeyringPair } from '@polkadot/keyring/types'; import { encodeAddress, ethereumEncode } from '@polkadot/util-crypto'; import { hexToU8a, u8aToHex } from '@polkadot/util'; @@ -8,17 +9,44 @@ import { Keyring } from '@polkadot/api'; import { Keypair } from '@polkadot/util-crypto/types'; import { Address20MultiAddress } from './helpers'; +/** + * Returns unified 32 bytes SS58 accountId + * @param pair + */ export function getUnifiedAddress(pair: KeyringPair): string { if ('ethereum' === pair.type) { const etheAddressHex = ethereumEncode(pair.publicKey); - return getConvertedEthereumAccount(etheAddressHex); + return getSS58AccountFromEthereumAccount(etheAddressHex); } if (pair.type === 'ecdsa') { - throw new Error(`ecdsa type is not supported!`); + throw new Error('Ecdsa key type is not supported and it should be replaced with ethereum ones!'); } return pair.address; } +/** + * Returns ethereum style public key with prefixed zeros example: 0x00000000000000000000000019a701d23f0ee1748b5d5f883cb833943096c6c4 + * @param pair + */ +export function getUnifiedPublicKey(pair: KeyringPair): Uint8Array { + if ('ethereum' === pair.type) { + const publicKeyBytes = hexToU8a(ethereumEncode(pair.publicKey)); + const result = new Uint8Array(32); + result.fill(0, 0, 12); + result.set(publicKeyBytes, 12); + return result; + } + if (pair.type === 'ecdsa') { + throw new Error('Ecdsa key type is not supported and it should be replaced with ethereum ones!'); + } + return pair.publicKey; +} + +/** + * This custom signer can get used to mimic EIP-191 message signing. By replacing the `ethereumPair.sign` with + * any wallet call we can sign any extrinsic with any wallet + * @param ethereumPair + */ export function getEthereumStyleSigner(ethereumPair: KeyringPair): Signer { return { signRaw: async (payload): Promise<SignerResult> => { @@ -35,48 +63,37 @@ export function getEthereumStyleSigner(ethereumPair: KeyringPair): Signer { } /** - * This is a helper method to allow being able to create a signature that might be created by metamask - * @param hexPayload - */ -function wrapCustomFrequencyTag(hexPayload: string): Uint8Array { - // wrapping in frequency tags to show this is a Frequency related payload - const frequencyWrapped = `<Frequency>${hexPayload.toLowerCase()}</Frequency>`; - return prefixEthereumTags(frequencyWrapped); -} - -/** - * prefixing with the EIP-191 for personal_sign messages (this gets wrapped automatically in metamask) - * @param hexPayload + * Convert a keyPair into a 20 byte ethereum address + * @param pair */ -function prefixEthereumTags(hexPayload: string): Uint8Array { - const wrapped = `\x19Ethereum Signed Message:\n${hexPayload.length}${hexPayload}`; - const buffer = Buffer.from(wrapped, 'utf-8'); - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); -} - export function getAccountId20MultiAddress(pair: KeyringPair): Address20MultiAddress { + if (pair.type !== 'ethereum') { + throw new Error(`Only ethereum keys are supported!`); + } const etheAddress = ethereumEncode(pair.publicKey); const ethAddress20 = Array.from(hexToU8a(etheAddress)); return { Address20: ethAddress20 }; } /** - * Returns ethereum style public key with prefixed zeros example: 0x00000000000000000000000019a701d23f0ee1748b5d5f883cb833943096c6c4 - * @param pair + * + * @param secretKey of secp256k1 keypair exported from any wallet (should be 32 bytes) */ -export function getConvertedEthereumPublicKey(pair: KeyringPair): Uint8Array { - const publicKeyBytes = hexToU8a(ethereumEncode(pair.publicKey)); - const result = new Uint8Array(32); - result.fill(0, 0, 12); - result.set(publicKeyBytes, 12); - return result; +export function getKeyringPairFromSecp256k1PrivateKey(secretKey: Uint8Array): KeyringPair { + const publicKey = secp256k1.getPublicKey(secretKey, true); + const keypair: Keypair = { + secretKey, + publicKey, + }; + const keyring = new Keyring({ type: 'ethereum' }); + return keyring.addFromPair(keypair, undefined, 'ethereum'); } /** * converts an ethereum account to SS58 format * @param accountId20Hex */ -function getConvertedEthereumAccount(accountId20Hex: string): string { +function getSS58AccountFromEthereumAccount(accountId20Hex: string): string { const addressBytes = hexToU8a(accountId20Hex); const result = new Uint8Array(32); result.fill(0, 0, 12); @@ -85,15 +102,21 @@ function getConvertedEthereumAccount(accountId20Hex: string): string { } /** - * - * @param secretKey of secp256k1 keypair exported from any wallet (should be 32 bytes) + * This is a helper method to allow being able to create a signature that might be created by Metamask + * @param hexPayload */ -export function getKeyringPairFromSecp256k1PrivateKey(secretKey: Uint8Array): KeyringPair { - const publicKey = secp256k1.getPublicKey(secretKey, true); - const keypair: Keypair = { - secretKey, - publicKey, - }; - const keyring = new Keyring({ type: 'ethereum' }); - return keyring.addFromPair(keypair, undefined, 'ethereum'); +function wrapCustomFrequencyTag(hexPayload: string): Uint8Array { + // wrapping in frequency tags to show this is a Frequency related payload + const frequencyWrapped = `<Frequency>${hexPayload.toLowerCase()}</Frequency>`; + return prefixEthereumTags(frequencyWrapped); +} + +/** + * prefixing with the EIP-191 for personal_sign messages (this gets wrapped automatically in metamask) + * @param hexPayload + */ +function prefixEthereumTags(hexPayload: string): Uint8Array { + const wrapped = `\x19Ethereum Signed Message:\n${hexPayload.length}${hexPayload}`; + const buffer = Buffer.from(wrapped, 'utf-8'); + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); } diff --git a/e2e/scaffolding/extrinsicHelpers.ts b/e2e/scaffolding/extrinsicHelpers.ts index aff5840827..3c0231dabe 100644 --- a/e2e/scaffolding/extrinsicHelpers.ts +++ b/e2e/scaffolding/extrinsicHelpers.ts @@ -6,7 +6,7 @@ import { Compact, u128, u16, u32, u64, Vec, Option, Bool } from '@polkadot/types import { FrameSystemAccountInfo, PalletPasskeyPasskeyPayload, 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, Sr25519Signature } from './helpers'; +import { getBlockNumber, getExistentialDeposit, getFinalizedBlockNumber, log, MultiSignatureType } from './helpers'; import autoNonce, { AutoNonce } from './autoNonce'; import { connect, connectPromise } from './apiConnection'; import { DispatchError, Event, Index, SignedBlock } from '@polkadot/types/interfaces'; @@ -24,7 +24,7 @@ import { u8aToHex } from '@polkadot/util/u8a/toHex'; import { u8aWrapBytes } from '@polkadot/util'; import type { AccountId32, Call, H256 } from '@polkadot/types/interfaces/runtime'; import { hasRelayChain } from './env'; -import { getUnifiedAddress } from './ethereum'; +import { getUnifiedAddress, getUnifiedPublicKey } from './ethereum'; export interface ReleaseSchedule { start: number; @@ -234,7 +234,7 @@ export class Extrinsic<N = unknown, T extends ISubmittableResult = ISubmittableR public getEstimatedTxFee(): Promise<bigint> { return firstValueFrom( this.extrinsic() - .paymentInfo(this.keys) + .paymentInfo(getUnifiedAddress(this.keys)) .pipe(map((info) => info.partialFee.toBigInt())) ); } @@ -479,7 +479,7 @@ export class ExtrinsicHelper { return new Extrinsic( () => ExtrinsicHelper.api.tx.schemas.createSchemaViaGovernance( - keys.publicKey, + getUnifiedPublicKey(keys), JSON.stringify(model), modelType, payloadLocation, @@ -502,7 +502,7 @@ export class ExtrinsicHelper { return new Extrinsic( () => ExtrinsicHelper.api.tx.schemas.createSchemaViaGovernanceV2( - keys.publicKey, + getUnifiedPublicKey(keys), JSON.stringify(model), modelType, payloadLocation, @@ -526,12 +526,13 @@ export class ExtrinsicHelper { public static addPublicKeyToMsa( keys: KeyringPair, - ownerSignature: Sr25519Signature, - newSignature: Sr25519Signature, + ownerSignature: MultiSignatureType, + newSignature: MultiSignatureType, payload: AddKeyData ) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.msa.addPublicKeyToMsa(keys.publicKey, ownerSignature, newSignature, payload), + () => + ExtrinsicHelper.api.tx.msa.addPublicKeyToMsa(getUnifiedPublicKey(keys), ownerSignature, newSignature, payload), keys, ExtrinsicHelper.api.events.msa.PublicKeyAdded ); @@ -560,12 +561,16 @@ export class ExtrinsicHelper { public static createSponsoredAccountWithDelegation( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: AddProviderPayload ) { return new Extrinsic( () => - ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation(delegatorKeys.publicKey, signature, payload), + ExtrinsicHelper.api.tx.msa.createSponsoredAccountWithDelegation( + getUnifiedPublicKey(delegatorKeys), + signature, + payload + ), providerKeys, ExtrinsicHelper.api.events.msa.MsaCreated ); @@ -574,11 +579,11 @@ export class ExtrinsicHelper { public static grantDelegation( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: AddProviderPayload ) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.msa.grantDelegation(delegatorKeys.publicKey, signature, payload), + () => ExtrinsicHelper.api.tx.msa.grantDelegation(getUnifiedPublicKey(delegatorKeys), signature, payload), providerKeys, ExtrinsicHelper.api.events.msa.DelegationGranted ); @@ -666,13 +671,13 @@ export class ExtrinsicHelper { public static applyItemActionsWithSignature( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: ItemizedSignaturePayload ) { return new Extrinsic( () => ExtrinsicHelper.api.tx.statefulStorage.applyItemActionsWithSignature( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signature, payload ), @@ -684,13 +689,13 @@ export class ExtrinsicHelper { public static applyItemActionsWithSignatureV2( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: ItemizedSignaturePayloadV2 ) { return new Extrinsic( () => ExtrinsicHelper.api.tx.statefulStorage.applyItemActionsWithSignatureV2( - delegatorKeys.publicKey, + getUnifiedPublicKey(delegatorKeys), signature, payload ), @@ -702,11 +707,16 @@ export class ExtrinsicHelper { public static deletePageWithSignature( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: PaginatedDeleteSignaturePayload ) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.statefulStorage.deletePageWithSignature(delegatorKeys.publicKey, signature, payload), + () => + ExtrinsicHelper.api.tx.statefulStorage.deletePageWithSignature( + getUnifiedPublicKey(delegatorKeys), + signature, + payload + ), providerKeys, ExtrinsicHelper.api.events.statefulStorage.PaginatedPageDeleted ); @@ -715,12 +725,16 @@ export class ExtrinsicHelper { public static deletePageWithSignatureV2( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: PaginatedDeleteSignaturePayloadV2 ) { return new Extrinsic( () => - ExtrinsicHelper.api.tx.statefulStorage.deletePageWithSignatureV2(delegatorKeys.publicKey, signature, payload), + ExtrinsicHelper.api.tx.statefulStorage.deletePageWithSignatureV2( + getUnifiedPublicKey(delegatorKeys), + signature, + payload + ), providerKeys, ExtrinsicHelper.api.events.statefulStorage.PaginatedPageDeleted ); @@ -729,11 +743,16 @@ export class ExtrinsicHelper { public static upsertPageWithSignature( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: PaginatedUpsertSignaturePayload ) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.statefulStorage.upsertPageWithSignature(delegatorKeys.publicKey, signature, payload), + () => + ExtrinsicHelper.api.tx.statefulStorage.upsertPageWithSignature( + getUnifiedPublicKey(delegatorKeys), + signature, + payload + ), providerKeys, ExtrinsicHelper.api.events.statefulStorage.PaginatedPageUpdated ); @@ -742,12 +761,16 @@ export class ExtrinsicHelper { public static upsertPageWithSignatureV2( delegatorKeys: KeyringPair, providerKeys: KeyringPair, - signature: Sr25519Signature, + signature: MultiSignatureType, payload: PaginatedUpsertSignaturePayloadV2 ) { return new Extrinsic( () => - ExtrinsicHelper.api.tx.statefulStorage.upsertPageWithSignatureV2(delegatorKeys.publicKey, signature, payload), + ExtrinsicHelper.api.tx.statefulStorage.upsertPageWithSignatureV2( + getUnifiedPublicKey(delegatorKeys), + signature, + payload + ), providerKeys, ExtrinsicHelper.api.events.statefulStorage.PaginatedPageUpdated ); @@ -772,7 +795,7 @@ export class ExtrinsicHelper { public static claimHandle(delegatorKeys: KeyringPair, payload: any) { const proof = { Sr25519: u8aToHex(delegatorKeys.sign(u8aWrapBytes(payload.toU8a()))) }; return new Extrinsic( - () => ExtrinsicHelper.api.tx.handles.claimHandle(delegatorKeys.publicKey, proof, payload), + () => ExtrinsicHelper.api.tx.handles.claimHandle(getUnifiedPublicKey(delegatorKeys), proof, payload), delegatorKeys, ExtrinsicHelper.api.events.handles.HandleClaimed ); diff --git a/e2e/scaffolding/funding.ts b/e2e/scaffolding/funding.ts index 927acb51e1..0f18fab281 100644 --- a/e2e/scaffolding/funding.ts +++ b/e2e/scaffolding/funding.ts @@ -21,16 +21,20 @@ export const fundingSources = [ 'capacity-unstaking', 'check-metadata-hash', 'frequency-misc', + 'frequency-balance-ethereum', 'handles', 'load-signature-registry', 'messages-add-ipfs', 'misc-util-batch', 'msa-create-msa', 'msa-key-management', + 'msa-key-management-ethereum', 'passkey-proxy', 'passkey-proxy-ethereum', + 'stateful-storage-ethereum', 'proxy-pallet', 'scenarios-grant-delegation', + 'grant-delegation-ethereum', 'schemas-create', 'stateful-storage-handle-itemized', 'stateful-storage-handle-paginated', diff --git a/e2e/scaffolding/helpers.ts b/e2e/scaffolding/helpers.ts index 733066478d..f5b081a3f2 100644 --- a/e2e/scaffolding/helpers.ts +++ b/e2e/scaffolding/helpers.ts @@ -57,6 +57,8 @@ export interface EcdsaSignature { Ecdsa: `0x${string}`; } +export type MultiSignatureType = Sr25519Signature | Ed25519Signature | EcdsaSignature; + export interface Address20MultiAddress { Address20: number[]; } @@ -79,6 +81,19 @@ export function signPayloadSr25519(keys: KeyringPair, data: Codec): Sr25519Signa return { Sr25519: u8aToHex(keys.sign(u8aWrapBytes(data.toU8a()))) }; } +export function signPayload(keys: KeyringPair, data: Codec): MultiSignatureType { + switch (keys.type) { + case 'ecdsa': + throw new Error('Ecdsa key type is not supported and it should be replaced with ethereum ones!'); + case 'sr25519': + return { Sr25519: u8aToHex(keys.sign(u8aWrapBytes(data.toU8a()))) }; + case 'ed25519': + return { Ed25519: u8aToHex(keys.sign(u8aWrapBytes(data.toU8a()))) }; + case 'ethereum': + return { Ecdsa: u8aToHex(keys.sign(data.toU8a())) }; + } +} + export async function generateDelegationPayload( payloadInputs: AddProviderPayload, expirationOffset: number = 100, @@ -148,7 +163,7 @@ export async function generateItemizedActionsPayloadAndSignature( ) { const payloadData = await generateItemizedSignaturePayload(payloadInput); const payload = ExtrinsicHelper.api.registry.createType(payloadType, payloadData); - const signature = signPayloadSr25519(signingKeys, payload); + const signature = signPayload(signingKeys, payload); return { payload: payloadData, signature }; } @@ -341,8 +356,12 @@ export async function createProviderKeysAndId(source: KeyringPair, amount?: bigi return [providerKeys, providerId]; } -export async function createDelegator(source: KeyringPair, amount?: bigint): Promise<[KeyringPair, u64]> { - const keys = await createAndFundKeypair(source, amount); +export async function createDelegator( + source: KeyringPair, + amount?: bigint, + keyType: KeypairType = 'sr25519' +): Promise<[KeyringPair, u64]> { + const keys = await createAndFundKeypair(source, amount, undefined, undefined, keyType); const createMsa = ExtrinsicHelper.createMsa(keys); const { target: msaCreatedEvent } = await createMsa.fundAndSend(source); const delegatorMsaId = msaCreatedEvent?.data.msaId || new u64(ExtrinsicHelper.api.registry, 0); @@ -354,11 +373,11 @@ export async function createDelegatorAndDelegation( source: KeyringPair, schemaId: u16 | u16[], providerId: u64, - providerKeys: KeyringPair + providerKeys: KeyringPair, + keyType: KeypairType = 'sr25519' ): Promise<[KeyringPair, u64]> { // Create a delegator msa - const [keys, delegatorMsaId] = await createDelegator(source); - + const [keys, delegatorMsaId] = await createDelegator(source, undefined, keyType); // Grant delegation to the provider const payload = await generateDelegationPayload({ authorizedMsaId: providerId, @@ -369,7 +388,7 @@ export async function createDelegatorAndDelegation( const grantDelegationOp = ExtrinsicHelper.grantDelegation( keys, providerKeys, - signPayloadSr25519(keys, addProviderData), + signPayload(keys, addProviderData), payload ); await grantDelegationOp.fundAndSend(source); @@ -666,8 +685,8 @@ export async function assertAddNewKey( newControlKeypair: KeyringPair ) { const addKeyPayloadCodec: Codec = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', addKeyPayload); - const ownerSig: Sr25519Signature = signPayloadSr25519(capacityKeys, addKeyPayloadCodec); - const newSig: Sr25519Signature = signPayloadSr25519(newControlKeypair, addKeyPayloadCodec); + const ownerSig: MultiSignatureType = signPayload(capacityKeys, addKeyPayloadCodec); + const newSig: MultiSignatureType = signPayload(newControlKeypair, addKeyPayloadCodec); const addPublicKeyOp = ExtrinsicHelper.addPublicKeyToMsa(capacityKeys, ownerSig, newSig, addKeyPayload); const { eventMap } = await addPublicKeyOp.signAndSend(); assertEvent(eventMap, 'system.ExtrinsicSuccess'); diff --git a/e2e/scenarios/grantDelegation.ethereum.test.ts b/e2e/scenarios/grantDelegation.ethereum.test.ts new file mode 100644 index 0000000000..357a569143 --- /dev/null +++ b/e2e/scenarios/grantDelegation.ethereum.test.ts @@ -0,0 +1,183 @@ +import '@frequency-chain/api-augment'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { u16, u64 } from '@polkadot/types'; +import assert from 'assert'; +import { AddProviderPayload, Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { + DOLLARS, + createAndFundKeypair, + createAndFundKeypairs, + generateDelegationPayload, + signPayload, +} from '../scaffolding/helpers'; +import { SchemaId } from '@frequency-chain/api-augment/interfaces'; +import { getFundingSource } from '../scaffolding/funding'; + +const fundingSource = getFundingSource('grant-delegation-ethereum'); + +describe('Delegation Scenario Tests Ethereum', function () { + let keys: KeyringPair; + let otherMsaKeys: KeyringPair; + let thirdMsaKeys: KeyringPair; + let noMsaKeys: KeyringPair; + let providerKeys: KeyringPair; + let otherProviderKeys: KeyringPair; + let schemaId: u16; + let schemaId2: SchemaId; + let providerId: u64; + let otherProviderId: u64; + let msaId: u64; + let otherMsaId: u64; + let thirdMsaId: u64; + + before(async function () { + // Fund all the different keys + [noMsaKeys, keys, otherMsaKeys, thirdMsaKeys, providerKeys, otherProviderKeys] = await createAndFundKeypairs( + fundingSource, + ['noMsaKeys', 'keys', 'otherMsaKeys', 'thirdMsaKeys', 'providerKeys', 'otherProviderKeys'], + 1n * DOLLARS, + 'ethereum' + ); + + const { target: msaCreatedEvent1 } = await ExtrinsicHelper.createMsa(keys).signAndSend(); + msaId = msaCreatedEvent1!.data.msaId; + + const { target: msaCreatedEvent2 } = await ExtrinsicHelper.createMsa(otherMsaKeys).signAndSend(); + otherMsaId = msaCreatedEvent2!.data.msaId; + + const { target: msaCreatedEvent3 } = await ExtrinsicHelper.createMsa(thirdMsaKeys).signAndSend(); + thirdMsaId = msaCreatedEvent3!.data.msaId; + + let createProviderMsaOp = ExtrinsicHelper.createMsa(providerKeys); + await createProviderMsaOp.signAndSend(); + let createProviderOp = ExtrinsicHelper.createProvider(providerKeys, 'MyPoster'); + let { target: providerEvent } = await createProviderOp.signAndSend(); + assert.notEqual(providerEvent, undefined, 'setup should return a ProviderCreated event'); + providerId = providerEvent!.data.providerId; + + createProviderMsaOp = ExtrinsicHelper.createMsa(otherProviderKeys); + await createProviderMsaOp.signAndSend(); + createProviderOp = ExtrinsicHelper.createProvider(otherProviderKeys, 'MyPoster'); + ({ target: providerEvent } = await createProviderOp.signAndSend()); + assert.notEqual(providerEvent, undefined, 'setup should return a ProviderCreated event'); + otherProviderId = providerEvent!.data.providerId; + + const schema = { + type: 'record', + name: 'Post', + fields: [ + { name: 'title', type: { name: 'Title', type: 'string' } }, + { name: 'content', type: { name: 'Content', type: 'string' } }, + { name: 'fromId', type: { name: 'DSNPId', type: 'fixed', size: 8 } }, + { name: 'objectId', type: 'DSNPId' }, + ], + }; + + schemaId = await ExtrinsicHelper.getOrCreateSchemaV3( + keys, + schema, + 'AvroBinary', + 'OnChain', + [], + 'test.grantDelegation' + ); + + schemaId2 = await ExtrinsicHelper.getOrCreateSchemaV3( + keys, + schema, + 'AvroBinary', + 'OnChain', + [], + 'test.grantDelegationSecond' + ); + }); + + describe('delegation grants for a Ethereum key', function () { + it('should fail to grant delegation if payload not signed by delegator (AddProviderSignatureVerificationFailed)', async function () { + const payload = await generateDelegationPayload({ + authorizedMsaId: providerId, + schemaIds: [schemaId], + }); + const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', payload); + + const grantDelegationOp = ExtrinsicHelper.grantDelegation( + keys, + providerKeys, + signPayload(providerKeys, addProviderData), + payload + ); + await assert.rejects(grantDelegationOp.fundAndSend(fundingSource), { + name: 'AddProviderSignatureVerificationFailed', + }); + }); + + it('should fail to grant delegation if ID in payload does not match origin (UnauthorizedDelegator)', async function () { + const payload = await generateDelegationPayload({ + authorizedMsaId: otherMsaId, + schemaIds: [schemaId], + }); + const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', payload); + + const grantDelegationOp = ExtrinsicHelper.grantDelegation( + keys, + providerKeys, + signPayload(keys, addProviderData), + payload + ); + await assert.rejects(grantDelegationOp.fundAndSend(fundingSource), { name: 'UnauthorizedDelegator' }); + }); + + it('should grant a delegation to a provider', async function () { + const payload = await generateDelegationPayload({ + authorizedMsaId: providerId, + schemaIds: [schemaId], + }); + const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', payload); + + const grantDelegationOp = ExtrinsicHelper.grantDelegation( + keys, + providerKeys, + signPayload(keys, addProviderData), + payload + ); + const { target: grantDelegationEvent } = await grantDelegationOp.fundAndSend(fundingSource); + assert.notEqual(grantDelegationEvent, undefined, 'should have returned DelegationGranted event'); + assert.deepEqual(grantDelegationEvent?.data.providerId, providerId, 'provider IDs should match'); + assert.deepEqual(grantDelegationEvent?.data.delegatorId, msaId, 'delegator IDs should match'); + }); + }); + + describe('createSponsoredAccountWithDelegation', function () { + let sponsorKeys: KeyringPair; + let op: Extrinsic; + let defaultPayload: AddProviderPayload; + + before(async function () { + sponsorKeys = await createAndFundKeypair(fundingSource, 50_000_000n, undefined, undefined, 'ethereum'); + defaultPayload = { + authorizedMsaId: providerId, + schemaIds: [schemaId], + }; + }); + + it('should successfully create a delegated account', async function () { + const payload = await generateDelegationPayload(defaultPayload); + const addProviderData = ExtrinsicHelper.api.registry.createType('PalletMsaAddProvider', payload); + + op = ExtrinsicHelper.createSponsoredAccountWithDelegation( + sponsorKeys, + providerKeys, + signPayload(sponsorKeys, addProviderData), + payload + ); + const { target: event, eventMap } = await op.fundAndSend(fundingSource); + assert.notEqual(event, undefined, 'should have returned MsaCreated event'); + assert.notEqual(eventMap['msa.DelegationGranted'], undefined, 'should have returned DelegationGranted event'); + await assert.rejects( + op.fundAndSend(fundingSource), + { name: 'SignatureAlreadySubmitted' }, + 'should reject double submission' + ); + }); + }); +}); diff --git a/e2e/stateful-pallet-storage/stateful.ethereum.test.ts b/e2e/stateful-pallet-storage/stateful.ethereum.test.ts new file mode 100644 index 0000000000..9cbe260efc --- /dev/null +++ b/e2e/stateful-pallet-storage/stateful.ethereum.test.ts @@ -0,0 +1,193 @@ +// E2E tests for pallets/stateful-pallet-storage/handleItemizedWithSignature.ts +import '@frequency-chain/api-augment'; +import assert from 'assert'; +import { + DOLLARS, + createDelegatorAndDelegation, + createProviderKeysAndId, + generateItemizedActions, + generateItemizedActionsSignedPayloadV2, + generatePaginatedDeleteSignaturePayloadV2, + generatePaginatedUpsertSignaturePayloadV2, + getCurrentPaginatedHash, + signPayload, +} from '../scaffolding/helpers'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { AVRO_CHAT_MESSAGE } from '../stateful-pallet-storage/fixtures/itemizedSchemaType'; +import { MessageSourceId, SchemaId } from '@frequency-chain/api-augment/interfaces'; +import { Bytes, u16 } from '@polkadot/types'; +import { getFundingSource } from '../scaffolding/funding'; + +const fundingSource = getFundingSource('stateful-storage-ethereum'); + +describe('📗 Stateful Pallet Storage Ethereum', function () { + let itemizedSchemaId: SchemaId; + let paginatedSchemaId: SchemaId; + let msa_id: MessageSourceId; + let undelegatedProviderId: MessageSourceId; + let undelegatedProviderKeys: KeyringPair; + let delegatedProviderId: MessageSourceId; + let delegatedProviderKeys: KeyringPair; + let ethereumDelegatorKeys: KeyringPair; + + before(async function () { + // Create a provider. This provider will NOT be granted delegations; + // methods requiring a payload signature do not require a delegation + [undelegatedProviderKeys, undelegatedProviderId] = await createProviderKeysAndId(fundingSource, 2n * DOLLARS); + assert.notEqual(undelegatedProviderId, undefined, 'setup should populate undelegatedProviderId'); + assert.notEqual(undelegatedProviderKeys, undefined, 'setup should populate undelegatedProviderKeys'); + + // Create a provider for the MSA, the provider will be used to grant delegation + [delegatedProviderKeys, delegatedProviderId] = await createProviderKeysAndId(fundingSource, 2n * DOLLARS); + assert.notEqual(delegatedProviderId, undefined, 'setup should populate delegatedProviderId'); + assert.notEqual(delegatedProviderKeys, undefined, 'setup should populate delegatedProviderKeys'); + + // Create a schema for Itemized PayloadLocation + itemizedSchemaId = await ExtrinsicHelper.getOrCreateSchemaV3( + undelegatedProviderKeys, + AVRO_CHAT_MESSAGE, + 'AvroBinary', + 'Itemized', + ['AppendOnly', 'SignatureRequired'], + 'test.ItemizedSignatureRequired' + ); + + // Create a schema for Paginated PayloadLocation + paginatedSchemaId = await ExtrinsicHelper.getOrCreateSchemaV3( + undelegatedProviderKeys, + AVRO_CHAT_MESSAGE, + 'AvroBinary', + 'Paginated', + ['SignatureRequired'], + 'test.PaginatedSignatureRequired' + ); + + // Create a MSA for the delegator + [ethereumDelegatorKeys, msa_id] = await createDelegatorAndDelegation( + fundingSource, + [itemizedSchemaId, paginatedSchemaId], + delegatedProviderId, + delegatedProviderKeys, + 'ethereum' + ); + console.log('after createDelegatorAndDelegation'); + assert.notEqual(ethereumDelegatorKeys, undefined, 'setup should populate delegator_key'); + assert.notEqual(msa_id, undefined, 'setup should populate msa_id'); + }); + + describe('Itemized With Signature Storage Tests', function () { + it('provider should be able to call applyItemizedActionWithSignatureV2 and apply actions with Ethereum keys', async function () { + const { payload, signature } = await generateItemizedActionsSignedPayloadV2( + generateItemizedActions([ + { action: 'Add', value: 'Hello, world from Frequency' }, + { action: 'Add', value: 'Hello, world again from Frequency' }, + ]), + itemizedSchemaId, + ethereumDelegatorKeys, + msa_id + ); + + const itemized_add_result_1 = ExtrinsicHelper.applyItemActionsWithSignatureV2( + ethereumDelegatorKeys, + undelegatedProviderKeys, + signature, + payload + ); + const { target: pageUpdateEvent1, eventMap: chainEvents } = + await itemized_add_result_1.fundAndSend(fundingSource); + assert.notEqual( + chainEvents['system.ExtrinsicSuccess'], + undefined, + 'should have returned an ExtrinsicSuccess event' + ); + assert.notEqual( + chainEvents['transactionPayment.TransactionFeePaid'], + undefined, + 'should have returned a TransactionFeePaid event' + ); + assert.notEqual( + pageUpdateEvent1, + undefined, + 'should have returned a PalletStatefulStorageItemizedActionApplied event' + ); + }); + }); + + describe('Paginated With Signature Storage Tests with Ethereum keys', function () { + it('provider should be able to call upsertPageWithSignatureV2 a page and deletePageWithSignatureV2 it successfully with Ethereum keys', async function () { + const page_id = new u16(ExtrinsicHelper.api.registry, 1); + + // Add and update actions + let target_hash = await getCurrentPaginatedHash(msa_id, paginatedSchemaId, page_id.toNumber()); + const upsertPayload = await generatePaginatedUpsertSignaturePayloadV2({ + targetHash: target_hash, + schemaId: paginatedSchemaId, + pageId: page_id, + payload: new Bytes(ExtrinsicHelper.api.registry, 'Hello World From Frequency'), + }); + const upsertPayloadData = ExtrinsicHelper.api.registry.createType( + 'PalletStatefulStoragePaginatedUpsertSignaturePayloadV2', + upsertPayload + ); + const upsert_result = ExtrinsicHelper.upsertPageWithSignatureV2( + ethereumDelegatorKeys, + undelegatedProviderKeys, + signPayload(ethereumDelegatorKeys, upsertPayloadData), + upsertPayload + ); + const { target: pageUpdateEvent, eventMap: chainEvents1 } = await upsert_result.fundAndSend(fundingSource); + assert.notEqual( + chainEvents1['system.ExtrinsicSuccess'], + undefined, + 'should have returned an ExtrinsicSuccess event' + ); + assert.notEqual( + chainEvents1['transactionPayment.TransactionFeePaid'], + undefined, + 'should have returned a TransactionFeePaid event' + ); + assert.notEqual( + pageUpdateEvent, + undefined, + 'should have returned a PalletStatefulStoragePaginatedPageUpdate event' + ); + + // Remove the page + target_hash = await getCurrentPaginatedHash(msa_id, paginatedSchemaId, page_id.toNumber()); + const deletePayload = await generatePaginatedDeleteSignaturePayloadV2({ + targetHash: target_hash, + schemaId: paginatedSchemaId, + pageId: page_id, + }); + const deletePayloadData = ExtrinsicHelper.api.registry.createType( + 'PalletStatefulStoragePaginatedDeleteSignaturePayloadV2', + deletePayload + ); + const remove_result = ExtrinsicHelper.deletePageWithSignatureV2( + ethereumDelegatorKeys, + undelegatedProviderKeys, + signPayload(ethereumDelegatorKeys, deletePayloadData), + deletePayload + ); + const { target: pageRemove, eventMap: chainEvents2 } = await remove_result.fundAndSend(fundingSource); + assert.notEqual( + chainEvents2['system.ExtrinsicSuccess'], + undefined, + 'should have returned an ExtrinsicSuccess event' + ); + assert.notEqual( + chainEvents2['transactionPayment.TransactionFeePaid'], + undefined, + 'should have returned a TransactionFeePaid event' + ); + assert.notEqual(pageRemove, undefined, 'should have returned a event'); + + // no pages should exist + const result = await ExtrinsicHelper.getPaginatedStorage(msa_id, paginatedSchemaId); + assert.notEqual(result, undefined, 'should have returned a valid response'); + const thePage = result.toArray().find((page) => page.page_id === page_id); + assert.equal(thePage, undefined, 'inserted page should not exist'); + }); + }); +});