Skip to content

Commit 9ecfa09

Browse files
authored
feat(TimeRelease): add ability to cancel scheduled transfers (#2292)
Add ability cancel a scheduled transfer by providing a unique identifier. When a transfer is canceled, any held funds are released. Scheduled transfers now require a unique identifier at the time of creation, which is used to reference and manage each transfer. Closes #2275
1 parent 003372c commit 9ecfa09

File tree

13 files changed

+545
-91
lines changed

13 files changed

+545
-91
lines changed

.github/workflows/common/codecov/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ runs:
99
steps:
1010
- name: Install grcov
1111
shell: bash
12-
run: cargo install --locked grcov
12+
run: cargo install --locked grcov --version 0.8.20
1313
- name: Build
1414
shell: bash # Limited to 10 threads max
1515
run: cargo build -j 10 --features frequency-lint-check

e2e/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/scaffolding/extrinsicHelpers.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { ApiPromise, ApiRx } from '@polkadot/api';
33
import { ApiTypes, AugmentedEvent, SubmittableExtrinsic, SignerOptions } from '@polkadot/api/types';
44
import { KeyringPair } from '@polkadot/keyring/types';
55
import { Compact, u128, u16, u32, u64, Vec, Option, Bool } from '@polkadot/types';
6-
import { FrameSystemAccountInfo, SpRuntimeDispatchError } from '@polkadot/types/lookup';
6+
import {
7+
FrameSystemAccountInfo,
8+
PalletTimeReleaseReleaseSchedule,
9+
SpRuntimeDispatchError,
10+
PalletSchedulerScheduled,
11+
} from '@polkadot/types/lookup';
712
import { AnyJson, AnyNumber, AnyTuple, Codec, IEvent, ISubmittableResult } from '@polkadot/types/types';
813
import { firstValueFrom, filter, map, pipe, tap } from 'rxjs';
914
import { getBlockNumber, getExistentialDeposit, getFinalizedBlockNumber, log, MultiSignatureType } from './helpers';
@@ -784,6 +789,28 @@ export class ExtrinsicHelper {
784789
);
785790
}
786791

792+
public static timeReleaseScheduleNamedTransfer(
793+
keys: KeyringPair,
794+
id: Uint8Array,
795+
who: KeyringPair,
796+
schedule: ReleaseSchedule,
797+
when: number
798+
) {
799+
return new Extrinsic(
800+
() => ExtrinsicHelper.api.tx.timeRelease.scheduleNamedTransfer(id, getUnifiedAddress(who), schedule, when),
801+
keys,
802+
ExtrinsicHelper.api.events.scheduler.Scheduled
803+
);
804+
}
805+
806+
public static timeReleaseCancelScheduledNamedTransfer(keys: KeyringPair, id: Uint8Array) {
807+
return new Extrinsic(
808+
() => ExtrinsicHelper.api.tx.timeRelease.cancelScheduledNamedTransfer(id),
809+
keys,
810+
ExtrinsicHelper.api.events.scheduler.Canceled
811+
);
812+
}
813+
787814
public static claimHandle(delegatorKeys: KeyringPair, payload: any) {
788815
const proof = { Sr25519: u8aToHex(delegatorKeys.sign(u8aWrapBytes(payload.toU8a()))) };
789816
return new Extrinsic(

e2e/time-release/timeRelease.test.ts

Lines changed: 89 additions & 1 deletion
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 { createAndFundKeypair } from '../scaffolding/helpers';
3+
import { createAndFundKeypair, getBlockNumber } from '../scaffolding/helpers';
44
import { KeyringPair } from '@polkadot/keyring/types';
55
import { ExtrinsicHelper, ReleaseSchedule } from '../scaffolding/extrinsicHelpers';
66
import { getFundingSource } from '../scaffolding/funding';
@@ -50,4 +50,92 @@ describe('TimeRelease', function () {
5050
assert.notEqual(target, undefined, 'should have returned ReleaseScheduleAdded event');
5151
});
5252
});
53+
54+
describe('Schedule transfers', function () {
55+
it('create a schedule transfer and cancel', async function () {
56+
const amount = 100000n * BigInt(DOLLARS);
57+
const schedule: ReleaseSchedule = calculateReleaseSchedule(amount);
58+
const currentBlock = await getBlockNumber();
59+
60+
const scheduleName1 = new Uint8Array(32).fill(1);
61+
const scheduleName2 = new Uint8Array(32).fill(2);
62+
63+
const scheduleTransferTx1 = ExtrinsicHelper.timeReleaseScheduleNamedTransfer(
64+
fundingSource,
65+
scheduleName1,
66+
vesterKeys,
67+
schedule,
68+
currentBlock + 20
69+
);
70+
const scheduleTransferTx2 = ExtrinsicHelper.timeReleaseScheduleNamedTransfer(
71+
fundingSource,
72+
scheduleName2,
73+
vesterKeys,
74+
schedule,
75+
currentBlock + 20
76+
);
77+
const { target: target1 } = await scheduleTransferTx1.signAndSend();
78+
const { target: target2 } = await scheduleTransferTx2.signAndSend();
79+
assert.notEqual(target1, undefined, 'should have returned Scheduled event');
80+
assert.notEqual(target2, undefined, 'should have returned Scheduled event');
81+
82+
const reservedAmountBefore =
83+
await ExtrinsicHelper.apiPromise.query.timeRelease.scheduleReservedAmounts(scheduleName1);
84+
85+
assert.equal('10,000,000,000,000', reservedAmountBefore.toHuman());
86+
87+
await ExtrinsicHelper.runToBlock(currentBlock + 2);
88+
89+
const cancelScheduleTransferTx1 = await ExtrinsicHelper.timeReleaseCancelScheduledNamedTransfer(
90+
fundingSource,
91+
scheduleName1
92+
);
93+
const { target: cancelTarget1 } = await cancelScheduleTransferTx1.signAndSend();
94+
assert.notEqual(cancelTarget1, undefined, 'should have returned scheduler Canceled event');
95+
96+
const reservedAmountAfter =
97+
await ExtrinsicHelper.apiPromise.query.timeRelease.scheduleReservedAmounts(scheduleName1);
98+
assert.equal(true, reservedAmountAfter.isEmpty, 'reserved amount should be empty');
99+
100+
const cancelScheduleTransferTx2 = await ExtrinsicHelper.timeReleaseCancelScheduledNamedTransfer(
101+
fundingSource,
102+
scheduleName2
103+
);
104+
105+
const { target: cancelTarget2 } = await cancelScheduleTransferTx2.signAndSend();
106+
107+
assert.notEqual(cancelTarget2, undefined, 'should have returned scheduler Canceled event');
108+
});
109+
});
110+
111+
describe('create schedule', function () {
112+
it('create a schedule transfer', async function () {
113+
const amount = 100000n * BigInt(DOLLARS);
114+
const schedule: ReleaseSchedule = calculateReleaseSchedule(amount);
115+
const currentBlock = await getBlockNumber();
116+
117+
const scheduleName1 = new Uint8Array(32).fill(1);
118+
119+
const scheduleTransferTx1 = ExtrinsicHelper.timeReleaseScheduleNamedTransfer(
120+
fundingSource,
121+
scheduleName1,
122+
vesterKeys,
123+
schedule,
124+
currentBlock + 10
125+
);
126+
const { target: target1 } = await scheduleTransferTx1.signAndSend();
127+
assert.notEqual(target1, undefined, 'should have returned Scheduled event');
128+
129+
const reservedAmountBefore =
130+
await ExtrinsicHelper.apiPromise.query.timeRelease.scheduleReservedAmounts(scheduleName1);
131+
132+
assert.equal('10,000,000,000,000', reservedAmountBefore.toHuman());
133+
134+
await ExtrinsicHelper.runToBlock(currentBlock + 15);
135+
136+
const reservedAmountAfter =
137+
await ExtrinsicHelper.apiPromise.query.timeRelease.scheduleReservedAmounts(scheduleName1);
138+
assert.equal(true, reservedAmountAfter.isEmpty, 'reserved amount should be empty');
139+
});
140+
});
53141
});

pallets/time-release/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ When a particular amount of tokens become eligible for release based on their ve
4040
| Name/Description | Caller | Payment | Key Events | Runtime Added |
4141
| ------------------------------------------------------------------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
4242
| `transfer`<br />Transfer tokens to another account with an unlock schedule | Token Account | Tokens | [`ReleaseScheduleAdded`](https://frequency-chain.github.io/frequency/pallet_time_release/pallet/enum.Event.html#variant.ReleaseScheduleAdded) | 24 |
43-
| `schedule_transfer`<br />Lock tokens in a sender's account for later execution of the `execute_scheduled_transfer` extrinsic at a pre-determined block number | Token Account | Tokens | \<see scheduler pallet\>| 144
44-
| `execute_scheduled_transfer`<br />Executes a scheduled transfer that was previously scheduled using the `schedule_transfer` extrinsic. | Scheduler pallet | Tokens | `ReleaseScheduleAdded` | 144
43+
| `schedule_transfer`<br />Lock tokens in a sender's account for later execution of the `execute_scheduled_named_transfer` extrinsic at a pre-determined block number | Token Account | Tokens | \<see scheduler pallet\>| 144
44+
| `execute_scheduled_named_transfer`<br />Executes a scheduled transfer that was previously scheduled using the `schedule_transfer` extrinsic. | Scheduler pallet | Tokens | `ReleaseScheduleAdded` | 144
4545
| `claim`<br />Remove the lock on tokens for the calling account when the schedule allows | Account with Lock | Tokens | [`Claimed`](https://frequency-chain.github.io/frequency/pallet_time_release/pallet/enum.Event.html#variant.Claimed) | 24 |
4646
| `claim_for`<br />Remove the lock on tokens for a different account when the schedule allows | Any Token Account | Tokens | [`Claimed`](https://frequency-chain.github.io/frequency/pallet_time_release/pallet/enum.Event.html#variant.Claimed) | 24 |
4747
| `update_release_schedules`<br />Governance action to update existing schedules | Governance | Tokens | [`ReleaseSchedulesUpdated`](https://frequency-chain.github.io/frequency/pallet_time_release/pallet/enum.Event.html#variant.ReleaseSchedulesUpdated) | 24 |

pallets/time-release/src/benchmarking.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ benchmarks! {
5555
);
5656
}
5757

58-
schedule_transfer {
58+
schedule_named_transfer {
5959
let schedule = Schedule::<T> {
6060
start: 0u32.into(),
6161
period: 2u32.into(),
@@ -72,15 +72,14 @@ benchmarks! {
7272
let to: T::AccountId = account("to", 1, SEED);
7373
let to_lookup = lookup_of_account::<T>(to.clone());
7474
let when = 10u32.into();
75-
76-
}: _(RawOrigin::Signed(from.clone()), to_lookup, schedule.clone(), when)
75+
}: _(RawOrigin::Signed(from.clone()), [1u8; 32], to_lookup, schedule.clone(), when)
7776
verify {
7877
assert_eq!(
7978
T::Currency::balance_on_hold(&HoldReason::TimeReleaseScheduledVesting.into(), &from),
8079
schedule.total_amount().unwrap());
8180
}
8281

83-
execute_scheduled_transfer {
82+
execute_scheduled_named_transfer {
8483
let schedule = Schedule::<T> {
8584
start: 0u32.into(),
8685
period: 2u32.into(),
@@ -101,7 +100,7 @@ benchmarks! {
101100

102101
let to_lookup = lookup_of_account::<T>(to.clone());
103102
let origin = Origin::<T>::TimeRelease(from.clone());
104-
}: _(origin, to_lookup, schedule.clone())
103+
}: _(origin, [1u8; 32],to_lookup, schedule.clone())
105104
verify {
106105
assert_eq!(
107106
T::Currency::total_balance(&to),
@@ -165,6 +164,36 @@ benchmarks! {
165164
);
166165
}
167166

167+
cancel_scheduled_named_transfer {
168+
let i in 1 .. (T::MaxReleaseSchedules::get());
169+
170+
let mut schedule = Schedule::<T> {
171+
start: 0u32.into(),
172+
period: 2u32.into(),
173+
period_count: 2,
174+
per_period: T::MinReleaseTransfer::get(),
175+
};
176+
177+
let from = T::AccountId::decode(&mut TrailingZeroInput::zeroes()).unwrap();
178+
set_balance::<T>(&from, schedule.total_amount().unwrap() * BalanceOf::<T>::from(i) + DOLLARS.into());
179+
180+
let to: T::AccountId = whitelisted_caller();
181+
let to_lookup = lookup_of_account::<T>(to.clone());
182+
183+
for j in 0..i {
184+
schedule.start = i.into();
185+
186+
TimeReleasePallet::<T>::schedule_named_transfer(RawOrigin::Signed(from.clone()).into(), [j as u8 ; 32], to_lookup.clone(), schedule.clone(), 4u32.into())?;
187+
}
188+
189+
let origin = RawOrigin::Signed(from.clone());
190+
let schedule_name = [0u8; 32];
191+
}: _(origin, schedule_name)
192+
verify {
193+
ensure!(ScheduleReservedAmounts::<T>::get([0u8 ; 32]).is_none(), "Schedule not canceled");
194+
}
195+
196+
168197
impl_benchmark_test_suite!(
169198
TimeReleasePallet,
170199
crate::mock::ExtBuilder::build(),

0 commit comments

Comments
 (0)