/
SingleAddressWallet.test.ts
156 lines (139 loc) · 6.33 KB
/
SingleAddressWallet.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { Cardano } from '@cardano-sdk/core';
import { SingleAddressWallet, StakeKeyStatus, Wallet } from '../../src';
import { assetProvider, keyManager, poolId1, poolId2, stakePoolSearchProvider, walletProvider } from './config';
import { distinctUntilChanged, filter, firstValueFrom, map, merge, mergeMap, skip, tap, timer } from 'rxjs';
const faucetAddress =
'addr_test1qqr585tvlc7ylnqvz8pyqwauzrdu0mxag3m7q56grgmgu7sxu2hyfhlkwuxupa9d5085eunq2qywy7hvmvej456flknswgndm3';
const createDelegationCertificates = async (wallet: Wallet) => {
const {
delegatee: delegateeBefore1stTx,
address: rewardAccount,
keyStatus
} = (await firstValueFrom(wallet.delegation.rewardAccounts$))[0];
// swap poolId if it's already delegating to one of the pools
const poolId = delegateeBefore1stTx?.nextNextEpoch.id === poolId2 ? poolId1 : poolId2;
const isStakeKeyRegistered = keyStatus === StakeKeyStatus.Registered;
const {
currentEpoch: { number: epoch }
} = await firstValueFrom(wallet.networkInfo$);
return {
certificates: [
...(isStakeKeyRegistered
? []
: ([
{ __typename: Cardano.CertificateType.StakeKeyRegistration, address: rewardAccount }
] as Cardano.Certificate[])),
{ __typename: Cardano.CertificateType.StakeDelegation, address: rewardAccount, epoch, poolId }
] as Cardano.Certificate[],
isStakeKeyRegistered,
poolId
};
};
const waitForNewStakePoolIdAfterTx = (wallet: Wallet) =>
firstValueFrom(
merge(
wallet.delegation.rewardAccounts$.pipe(
map(([acc]) => acc.delegatee?.nextNextEpoch.id),
distinctUntilChanged(),
skip(1)
),
wallet.transactions.outgoing.failed$,
// Test will fail if fetching new stake pool takes more than 30s
wallet.transactions.outgoing.confirmed$.pipe(mergeMap(() => timer(30_000)))
)
);
describe('SingleAddressWallet', () => {
let rewardAccount: Cardano.Address;
let wallet: Wallet;
const waitForBalanceCoins = (expectedCoins: Cardano.Lovelace) =>
firstValueFrom(
wallet.balance.total$.pipe(
filter(({ coins }) => coins === expectedCoins),
tap(({ coins }) => expect(wallet.balance.available$.value?.coins).toBe(coins))
)
);
beforeAll(async () => {
wallet = new SingleAddressWallet(
{ name: 'Test Wallet' },
{
assetProvider,
keyManager,
stakePoolSearchProvider,
walletProvider
}
);
[{ rewardAccount }] = await firstValueFrom(wallet.addresses$);
});
afterAll(() => wallet.shutdown());
it('has an address', () => {
expect(wallet.addresses$.value![0].address.startsWith('addr')).toBe(true);
});
it('has assets$', async () => {
expect(typeof (await firstValueFrom(wallet.assets$))).toBe('object');
});
test('balance & transaction', async () => {
const stakeKeyDeposit = BigInt((await firstValueFrom(wallet.protocolParameters$)).stakeKeyDeposit);
const initialTotalBalance = await firstValueFrom(wallet.balance.total$);
const initialAvailableBalance = await firstValueFrom(wallet.balance.available$);
expect(initialTotalBalance.coins).toBeGreaterThan(0n);
expect(initialTotalBalance.coins).toBe(initialAvailableBalance.coins);
const tx1OutputCoins = 1_000_000n;
const { poolId, certificates, isStakeKeyRegistered } = await createDelegationCertificates(wallet);
const initialDeposit = isStakeKeyRegistered ? stakeKeyDeposit : 0n;
expect(initialAvailableBalance.deposit).toBe(initialDeposit);
const [initialDelegatee] = await firstValueFrom(wallet.delegation.rewardAccounts$);
// Make a 1st tx with key registration (if not already registered) and stake delegation
// Also send some coin to faucet
const tx1Internals = await wallet.initializeTx({
certificates,
outputs: new Set([{ address: faucetAddress, value: { coins: tx1OutputCoins } }])
});
await wallet.submitTx(await wallet.finalizeTx(tx1Internals));
const expectedCoinsAfterTx1 =
initialTotalBalance.coins -
tx1OutputCoins -
tx1Internals.body.fee -
(isStakeKeyRegistered ? 0n : stakeKeyDeposit);
await firstValueFrom(wallet.transactions.outgoing.inFlight$.pipe(filter((txs) => txs.length > 0)));
// Assert changes after submitting the tx
await Promise.all([
// Test it locks available balance after tx is submitted
// and updates total balance after tx is confirmed
(async () => {
const afterTx1TotalBalance = await firstValueFrom(wallet.balance.total$);
const afterTx1AvailableBalance = await firstValueFrom(wallet.balance.available$);
expect(afterTx1TotalBalance.coins).toBe(initialTotalBalance.coins);
const utxo = wallet.utxo.total$.value!;
const expectedCoinsWhileTxPending =
initialTotalBalance.coins -
Cardano.util.coalesceValueQuantities(
tx1Internals.body.inputs.map((txInput) => utxo.find(([txIn]) => txIn.txId === txInput.txId)![1].value)
).coins;
expect(afterTx1AvailableBalance.coins).toBe(expectedCoinsWhileTxPending);
await waitForBalanceCoins(expectedCoinsAfterTx1);
})(),
// Test it updates wallet.delegation after delegating to stake pool
(async () => {
expect(await waitForNewStakePoolIdAfterTx(wallet)).toBe(poolId);
const [newDelegatee] = await firstValueFrom(wallet.delegation.rewardAccounts$);
// nothing changes for 2 epochs
expect(newDelegatee.delegatee?.nextEpoch).toEqual(initialDelegatee?.delegatee?.nextEpoch);
expect(newDelegatee.delegatee?.currentEpoch).toEqual(initialDelegatee?.delegatee?.currentEpoch);
})(),
// Test confirmed$
async () => {
expect(await firstValueFrom(wallet.transactions.outgoing.confirmed$)).toBeTruthy();
}
]);
// Make a 2nd tx with key deregistration
const tx2Internals = await wallet.initializeTx({
certificates: [{ __typename: Cardano.CertificateType.StakeKeyDeregistration, address: rewardAccount }]
});
await wallet.submitTx(await wallet.finalizeTx(tx2Internals));
// No longer delegating
expect(await waitForNewStakePoolIdAfterTx(wallet)).toBeUndefined();
// Deposit is returned to wallet balance
await waitForBalanceCoins(expectedCoinsAfterTx1 + stakeKeyDeposit - tx2Internals.body.fee);
expect((await firstValueFrom(wallet.balance.total$)).deposit).toBe(0n);
});
});