Skip to content

Commit 1528d19

Browse files
committed
feat: free extrinsic
- address some PR comments - make extrinsic free - update tests for free extrinsic
1 parent fa04839 commit 1528d19

File tree

10 files changed

+505
-171
lines changed

10 files changed

+505
-171
lines changed

e2e/msa/msaTokens.test.ts

Lines changed: 97 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import '@frequency-chain/api-augment';
22
import assert from 'assert';
3-
import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
3+
import { AuthorizedKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers';
44
import { ethereumAddressToKeyringPair, getUnifiedAddress, getUnifiedPublicKey } from '../scaffolding/ethereum';
55
import { getFundingSource } from '../scaffolding/funding';
66
import { H160 } from '@polkadot/types/interfaces';
@@ -12,7 +12,7 @@ import {
1212
createAndFundKeypair,
1313
createKeys,
1414
DOLLARS,
15-
generateAddKeyPayload,
15+
generateAuthorizedKeyPayload,
1616
getExistentialDeposit,
1717
signPayloadSr25519,
1818
Sr25519Signature,
@@ -115,127 +115,163 @@ describe('MSAs Holding Tokens', function () {
115115
});
116116

117117
describe('withdrawTokens', function () {
118-
let keys: KeyringPair;
118+
let msaKeys: KeyringPair;
119119
let msaId: u64;
120120
let msaAddress: H160;
121+
let otherMsaKeys: KeyringPair;
121122
let secondaryKey: KeyringPair;
122-
const defaultPayload: AddKeyData = {};
123-
let payload: AddKeyData;
123+
let defaultPayload: AuthorizedKeyData;
124+
let payload: AuthorizedKeyData;
124125
let ownerSig: Sr25519Signature;
125126
let badSig: Sr25519Signature;
126-
let addKeyData: Codec;
127+
let authorizedKeyData: Codec;
127128

128129
before(async function () {
129130
// Setup an MSA with tokens
130-
keys = await createAndFundKeypair(fundingSource, 5n * CENTS);
131-
const { target } = await ExtrinsicHelper.createMsa(keys).signAndSend();
131+
msaKeys = await createAndFundKeypair(fundingSource, 5n * CENTS);
132+
let { target } = await ExtrinsicHelper.createMsa(msaKeys).signAndSend();
132133
assert.notEqual(target?.data.msaId, undefined, 'MSA Id not in expected event');
133134
msaId = target!.data.msaId;
134135

136+
// Setup another MSA control key
137+
otherMsaKeys = await createAndFundKeypair(fundingSource, 5n * CENTS);
138+
({ target } = await ExtrinsicHelper.createMsa(otherMsaKeys).signAndSend());
139+
assert.notEqual(target?.data.msaId, undefined, 'MSA Id not in expected event');
140+
135141
const { accountId } = await ExtrinsicHelper.apiPromise.call.msaRuntimeApi.getEthereumAddressForMsaId(msaId);
136142
msaAddress = accountId;
137143

138-
secondaryKey = await createAndFundKeypair(fundingSource, 5n * CENTS);
144+
// Create unfunded keys; this extrinsic should be free
145+
secondaryKey = createKeys();
139146

140147
// Default payload making it easier to test `withdrawTokens`
141-
defaultPayload.msaId = msaId;
142-
defaultPayload.newPublicKey = getUnifiedPublicKey(secondaryKey);
148+
defaultPayload = {
149+
msaId,
150+
authorizedPublicKey: getUnifiedPublicKey(secondaryKey),
151+
};
143152
});
144153

145154
beforeEach(async function () {
146-
payload = await generateAddKeyPayload(defaultPayload);
155+
payload = await generateAuthorizedKeyPayload(defaultPayload);
147156
});
148157

149158
it('should fail if origin is not address contained in the payload (NotKeyOwner)', async function () {
150-
const badPayload = { ...payload, newPublicKey: getUnifiedAddress(createKeys()) }; // Invalid MSA ID
151-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', badPayload);
152-
ownerSig = signPayloadSr25519(keys, addKeyData);
153-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, badPayload);
154-
await assert.rejects(op.fundAndSend(fundingSource), {
155-
name: 'NotKeyOwner',
159+
const badPayload = { ...payload, authorizedPublicKey: getUnifiedAddress(createKeys()) }; // Invalid MSA ID
160+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', badPayload);
161+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
162+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, badPayload);
163+
await assert.rejects(op.signAndSend(), {
164+
name: 'RpcError',
165+
code: 1010,
166+
data: 'Custom error: 5', // NotKeyOwner,
156167
});
157168
});
158169

159170
it('should fail if MSA owner signature is invalid (MsaOwnershipInvalidSignature)', async function () {
160-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload);
161-
badSig = signPayloadSr25519(createKeys(), addKeyData); // Invalid signature
162-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, badSig, payload);
163-
await assert.rejects(op.fundAndSend(fundingSource), {
164-
name: 'MsaOwnershipInvalidSignature',
171+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', payload);
172+
badSig = signPayloadSr25519(createKeys(), authorizedKeyData); // Invalid signature
173+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, badSig, payload);
174+
await assert.rejects(op.signAndSend(), {
175+
name: 'RpcError',
176+
code: 1010,
177+
data: 'Custom error: 8', // MsaOwnershipInvalidSignature
165178
});
166179
});
167180

168181
it('should fail if expiration has passed (ProofHasExpired)', async function () {
169-
const newPayload = await generateAddKeyPayload({
182+
const newPayload = await generateAuthorizedKeyPayload({
170183
...defaultPayload,
171184
expiration: (await ExtrinsicHelper.getLastBlock()).block.header.number.toNumber(),
172185
});
173-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
174-
ownerSig = signPayloadSr25519(keys, addKeyData);
175-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
176-
await assert.rejects(op.fundAndSend(fundingSource), {
177-
name: 'ProofHasExpired',
186+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', newPayload);
187+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
188+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, newPayload);
189+
await assert.rejects(op.signAndSend(), {
190+
name: 'RpcError',
191+
code: 1010,
192+
data: 'Custom error: 14', // ProofHasExpired,
178193
});
179194
});
180195

181196
it('should fail if expiration is not yet valid (ProofNotYetValid)', async function () {
182197
const maxMortality = ExtrinsicHelper.api.consts.msa.mortalityWindowSize.toNumber();
183-
const newPayload = await generateAddKeyPayload({
198+
const newPayload = await generateAuthorizedKeyPayload({
184199
...defaultPayload,
185200
expiration: (await ExtrinsicHelper.getLastBlock()).block.header.number.toNumber() + maxMortality + 999,
186201
});
187-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
188-
ownerSig = signPayloadSr25519(keys, addKeyData);
189-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
190-
await assert.rejects(op.fundAndSend(fundingSource), {
191-
name: 'ProofNotYetValid',
202+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', newPayload);
203+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
204+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, newPayload);
205+
await assert.rejects(op.signAndSend(), {
206+
name: 'RpcError',
207+
code: 1010,
208+
data: 'Custom error: 13', // ProofNotYetValid,
209+
});
210+
});
211+
212+
it('should fail if origin is an MSA control key (IneligibleOrigin)', async function () {
213+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', payload);
214+
const newPayload = await generateAuthorizedKeyPayload({
215+
...defaultPayload,
216+
authorizedPublicKey: getUnifiedPublicKey(otherMsaKeys),
217+
});
218+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', newPayload);
219+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
220+
const op = ExtrinsicHelper.withdrawTokens(otherMsaKeys, msaKeys, ownerSig, newPayload);
221+
await assert.rejects(op.signAndSend(), {
222+
name: 'RpcError',
223+
code: 1010,
224+
data: 'Custom error: 12', // IneligibleOrigin,
192225
});
193226
});
194227

195228
it('should fail if payload signer does not control the MSA in the signed payload (NotMsaOwner)', async function () {
196-
const newPayload = await generateAddKeyPayload({
229+
const newPayload = await generateAuthorizedKeyPayload({
197230
...defaultPayload,
198231
msaId: new u64(ExtrinsicHelper.api.registry, 9999),
199232
});
200-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
201-
ownerSig = signPayloadSr25519(keys, addKeyData);
202-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, newPayload);
203-
await assert.rejects(op.fundAndSend(fundingSource), {
204-
name: 'NotMsaOwner',
233+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', newPayload);
234+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
235+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, newPayload);
236+
await assert.rejects(op.signAndSend(), {
237+
name: 'RpcError',
238+
code: 1010,
239+
data: 'Custom error: 17', // NotMsaOwner,
205240
});
206241
});
207242

208243
it('should fail if payload signer is not an MSA control key (NoKeyExists)', async function () {
209244
const badSigner = createKeys();
210-
const newPayload = await generateAddKeyPayload({
245+
const newPayload = await generateAuthorizedKeyPayload({
211246
...defaultPayload,
212247
msaId: new u64(ExtrinsicHelper.api.registry, 9999),
213248
});
214-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
215-
ownerSig = signPayloadSr25519(badSigner, addKeyData);
249+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', newPayload);
250+
ownerSig = signPayloadSr25519(badSigner, authorizedKeyData);
216251
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, badSigner, ownerSig, newPayload);
217-
await assert.rejects(op.fundAndSend(fundingSource), {
218-
name: 'NoKeyExists',
252+
await assert.rejects(op.signAndSend(), {
253+
name: 'RpcError',
254+
code: 1010,
255+
data: 'Custom error: 16', // NoKeyExists,
219256
});
220257
});
221258

222259
it('should fail if MSA does not have a balance', async function () {
223-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', payload);
224-
ownerSig = signPayloadSr25519(keys, addKeyData);
225-
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, keys, ownerSig, payload);
226-
await assert.rejects(op.fundAndSend(fundingSource), {
227-
name: 'InsufficientBalanceToWithdraw',
260+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', payload);
261+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
262+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, payload);
263+
await assert.rejects(op.signAndSend(), {
264+
name: 'RpcError',
265+
code: 1010,
266+
data: 'Custom error: 9', // InsufficientBalanceToWithdraw,
228267
});
229268
});
230269

231270
it('should succeed', async function () {
232-
// Fund receiver with known amount to pay for transaction
233-
const startingAmount = 1n * DOLLARS;
234271
const transferAmount = 1n * DOLLARS;
235-
const tertiaryKeys = await createAndFundKeypair(fundingSource, startingAmount);
236272
const {
237273
data: { free: startingBalance },
238-
} = await ExtrinsicHelper.getAccountInfo(tertiaryKeys);
274+
} = await ExtrinsicHelper.getAccountInfo(secondaryKey);
239275

240276
// Send tokens to MSA
241277
try {
@@ -249,20 +285,18 @@ describe('MSAs Holding Tokens', function () {
249285
console.error('Error sending tokens to MSA', err.message);
250286
}
251287

252-
const newPayload = await generateAddKeyPayload({ ...payload, newPublicKey: getUnifiedPublicKey(tertiaryKeys) });
253-
addKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAddKeyData', newPayload);
254-
ownerSig = signPayloadSr25519(keys, addKeyData);
255-
const op = ExtrinsicHelper.withdrawTokens(tertiaryKeys, keys, ownerSig, newPayload);
256-
const { eventMap } = await op.fundAndSend(fundingSource);
257-
const feeAmount = (eventMap['transactionPayment.TransactionFeePaid'].data as unknown as any).actualFee;
288+
authorizedKeyData = ExtrinsicHelper.api.registry.createType('PalletMsaAuthorizedKeyData', payload);
289+
ownerSig = signPayloadSr25519(msaKeys, authorizedKeyData);
290+
const op = ExtrinsicHelper.withdrawTokens(secondaryKey, msaKeys, ownerSig, payload);
291+
await assert.doesNotReject(op.signAndSend(), 'token transfer transaction should have succeeded');
258292

259293
// Destination account should have had balance increased
260294
const {
261295
data: { free: endingBalance },
262-
} = await ExtrinsicHelper.getAccountInfo(tertiaryKeys);
296+
} = await ExtrinsicHelper.getAccountInfo(secondaryKey);
263297

264298
assert(
265-
startingBalance.toBigInt() + transferAmount - feeAmount.toBigInt() === endingBalance.toBigInt(),
299+
startingBalance.toBigInt() + transferAmount === endingBalance.toBigInt(),
266300
'balance of recieve should have increased by the transfer amount minus fee'
267301
);
268302
});

e2e/scaffolding/extrinsicHelpers.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@ import { ApiPromise, ApiRx } from '@polkadot/api';
44
import { ApiTypes, AugmentedEvent, SubmittableExtrinsic, SignerOptions } from '@polkadot/api/types';
55
import { KeyringPair } from '@polkadot/keyring/types';
66
import { Compact, u128, u16, u32, u64, Vec, Option, Bool } from '@polkadot/types';
7-
import {
8-
FrameSystemAccountInfo,
9-
PalletTimeReleaseReleaseSchedule,
10-
SpRuntimeDispatchError,
11-
PalletSchedulerScheduled,
12-
} from '@polkadot/types/lookup';
7+
import { FrameSystemAccountInfo, SpRuntimeDispatchError } from '@polkadot/types/lookup';
138
import { AnyJson, AnyNumber, AnyTuple, Codec, IEvent, ISubmittableResult } from '@polkadot/types/types';
149
import { firstValueFrom, filter, map, pipe, tap } from 'rxjs';
1510
import { getBlockNumber, getExistentialDeposit, getFinalizedBlockNumber, log, MultiSignatureType } from './helpers';
@@ -31,6 +26,7 @@ import { u8aWrapBytes } from '@polkadot/util';
3126
import type { AccountId32, Call, H256 } from '@polkadot/types/interfaces/runtime';
3227
import { hasRelayChain } from './env';
3328
import { getUnifiedAddress, getUnifiedPublicKey } from './ethereum';
29+
import { RpcErrorInterface } from '@polkadot/rpc-provider/types';
3430

3531
export interface ReleaseSchedule {
3632
start: number;
@@ -44,6 +40,11 @@ export interface AddKeyData {
4440
expiration?: any;
4541
newPublicKey?: any;
4642
}
43+
export interface AuthorizedKeyData {
44+
msaId: u64;
45+
expiration?: AnyNumber;
46+
authorizedPublicKey: KeyringPair['publicKey'];
47+
}
4748
export interface AddProviderPayload {
4849
authorizedMsaId?: u64;
4950
schemaIds?: u16[];
@@ -91,6 +92,10 @@ export interface PaginatedDeleteSignaturePayloadV2 {
9192
expiration?: any;
9293
}
9394

95+
export function isRpcError<T = string>(e: any): e is RpcErrorInterface<T> {
96+
return e?.name === 'RpcError';
97+
}
98+
9499
export class EventError extends Error {
95100
name: string = '';
96101
message: string = '';
@@ -188,6 +193,7 @@ export class Extrinsic<N = unknown, T extends ISubmittableResult = ISubmittableR
188193
try {
189194
const op = this.extrinsic();
190195
// Era is 0 for tests due to issues with BirthBlock
196+
// eslint-disable-next-line no-restricted-syntax
191197
return await firstValueFrom(
192198
op.signAndSend(this.keys, { nonce, era: 0, ...options }).pipe(
193199
tap((result) => {
@@ -205,8 +211,11 @@ export class Extrinsic<N = unknown, T extends ISubmittableResult = ISubmittableR
205211
)
206212
);
207213
} catch (e) {
208-
if ((e as any).name === 'RpcError' && inputNonce === 'auto') {
209-
console.error("WARNING: Unexpected RPC Error! If it is expected, use 'current' for the nonce.");
214+
if (isRpcError(e)) {
215+
if (inputNonce === 'auto') {
216+
console.error("WARNING: Unexpected RPC Error! If it is expected, use 'current' for the nonce.");
217+
}
218+
log(`RpcError:`, { code: e.code, data: e.data });
210219
}
211220
throw e;
212221
}

e2e/scaffolding/helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import {
1717
AddKeyData,
1818
AddProviderPayload,
19+
AuthorizedKeyData,
1920
EventMap,
2021
ExtrinsicHelper,
2122
ItemizedSignaturePayloadV2,
@@ -182,6 +183,18 @@ export async function generateAddKeyPayload(
182183
};
183184
}
184185

186+
export async function generateAuthorizedKeyPayload(
187+
payloadInputs: AuthorizedKeyData,
188+
expirationOffset: number = 100,
189+
blockNumber?: number
190+
): Promise<AuthorizedKeyData> {
191+
const { expiration, ...payload } = payloadInputs;
192+
return {
193+
expiration: expiration || (blockNumber || (await getBlockNumber())) + expirationOffset,
194+
...payload,
195+
};
196+
}
197+
185198
export async function generateItemizedSignaturePayload(
186199
payloadInputs: ItemizedSignaturePayloadV2,
187200
expirationOffset: number = 100,

pallets/msa/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ sp-core = { workspace = true }
2525
sp-io = { workspace = true }
2626
sp-runtime = { workspace = true }
2727
sp-weights = { workspace = true }
28-
pallet-balances = { workspace = true }
2928
# Frequency related dependencies
3029
common-primitives = { default-features = false, path = "../../common/primitives" }
3130
serde = { workspace = true, features = ["derive"] }
@@ -34,6 +33,7 @@ hex = { workspace = true, default-features = false, features = ["alloc"] }
3433
common-runtime = { path = "../../runtime/common", default-features = false }
3534

3635
[dev-dependencies]
36+
pallet-balances = { workspace = true }
3737
pallet-schemas = { path = "../schemas", default-features = false }
3838
pallet-handles = { path = "../handles", default-features = false }
3939
pallet-collective = { workspace = true }

0 commit comments

Comments
 (0)