Skip to content

Commit 86b3f69

Browse files
feat(wallet): added SharedWallet implementation
1 parent 7708a4b commit 86b3f69

File tree

10 files changed

+1728
-1
lines changed

10 files changed

+1728
-1
lines changed

packages/e2e/src/factories.ts

+69
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ObservableWallet,
1010
PersonalWallet,
1111
PollingConfig,
12+
SharedWallet,
1213
SingleAddressDiscovery,
1314
storage
1415
} from '@cardano-sdk/wallet';
@@ -286,6 +287,18 @@ export type GetWalletProps = {
286287
witnesser?: Witnesser;
287288
};
288289

290+
export type GetSharedWalletProps = {
291+
env: any;
292+
logger: Logger;
293+
name: string;
294+
polling?: PollingConfig;
295+
handlePolicyIds?: Cardano.PolicyId[];
296+
stores?: storage.WalletStores;
297+
witnesser: Witnesser;
298+
paymentScript: Cardano.RequireAllOfScript | Cardano.RequireAnyOfScript | Cardano.RequireAtLeastScript;
299+
stakingScript: Cardano.RequireAllOfScript | Cardano.RequireAnyOfScript | Cardano.RequireAtLeastScript;
300+
};
301+
289302
/** Delays initializing tx when nearing the epoch boundary. Relies on system clock being accurate. */
290303
const patchInitializeTxToRespectEpochBoundary = <T extends ObservableWallet>(
291304
wallet: T,
@@ -381,4 +394,60 @@ export const getWallet = async (props: GetWalletProps) => {
381394
return { bip32Account, providers, wallet: patchInitializeTxToRespectEpochBoundary(wallet, maxInterval) };
382395
};
383396

397+
/**
398+
* Create a shared wallet instance given the environment variables.
399+
*
400+
* @param props Wallet configuration parameters.
401+
* @returns an object containing the wallet and providers passed to it
402+
*/
403+
export const getSharedWallet = async (props: GetSharedWalletProps) => {
404+
const { env, logger, name, polling, stores, paymentScript, stakingScript, witnesser } = props;
405+
const providers = {
406+
assetProvider: await assetProviderFactory.create(env.ASSET_PROVIDER, env.ASSET_PROVIDER_PARAMS, logger),
407+
chainHistoryProvider: await chainHistoryProviderFactory.create(
408+
env.CHAIN_HISTORY_PROVIDER,
409+
env.CHAIN_HISTORY_PROVIDER_PARAMS,
410+
logger
411+
),
412+
handleProvider: await handleProviderFactory.create(env.HANDLE_PROVIDER, env.HANDLE_PROVIDER_PARAMS, logger),
413+
networkInfoProvider: await networkInfoProviderFactory.create(
414+
env.NETWORK_INFO_PROVIDER,
415+
env.NETWORK_INFO_PROVIDER_PARAMS,
416+
logger
417+
),
418+
rewardsProvider: await rewardsProviderFactory.create(env.REWARDS_PROVIDER, env.REWARDS_PROVIDER_PARAMS, logger),
419+
stakePoolProvider: await stakePoolProviderFactory.create(
420+
env.STAKE_POOL_PROVIDER,
421+
env.STAKE_POOL_PROVIDER_PARAMS,
422+
logger
423+
),
424+
txSubmitProvider: await txSubmitProviderFactory.create(
425+
env.TX_SUBMIT_PROVIDER,
426+
env.TX_SUBMIT_PROVIDER_PARAMS,
427+
logger
428+
),
429+
utxoProvider: await utxoProviderFactory.create(env.UTXO_PROVIDER, env.UTXO_PROVIDER_PARAMS, logger)
430+
};
431+
const wallet = new SharedWallet(
432+
{ name, polling },
433+
{
434+
...providers,
435+
logger,
436+
paymentScript,
437+
stakingScript,
438+
stores,
439+
witnesser
440+
}
441+
);
442+
443+
const [{ address, rewardAccount }] = await firstValueFrom(wallet.addresses$);
444+
logger.info(`Created wallet "${wallet.name}": ${address}/${rewardAccount}`);
445+
446+
const maxInterval =
447+
polling?.maxInterval ||
448+
(polling?.interval && polling.interval * DEFAULT_POLLING_CONFIG.maxIntervalMultiplier) ||
449+
DEFAULT_POLLING_CONFIG.maxInterval;
450+
return { providers, wallet: patchInitializeTxToRespectEpochBoundary(wallet, maxInterval) };
451+
};
452+
384453
export type TestWallet = Awaited<ReturnType<typeof getWallet>>;

packages/e2e/src/util/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export const runningAgainstLocalNetwork = async () => {
189189
* @param tx The **already confirmed** transaction we need to know the confirmation epoch
190190
* @returns The epoch when the given transaction was confirmed
191191
*/
192-
export const getTxConfirmationEpoch = async (wallet: PersonalWallet, tx: Cardano.Tx<Cardano.TxBody>) => {
192+
export const getTxConfirmationEpoch = async (wallet: ObservableWallet, tx: Cardano.Tx<Cardano.TxBody>) => {
193193
const txs = await firstValueFrom(wallet.transactions.history$.pipe(filter((_) => _.some(({ id }) => id === tx.id))));
194194
const observedTx = txs.find(({ id }) => id === tx.id);
195195
const slotEpochCalc = createSlotEpochCalc(await firstValueFrom(wallet.eraSummaries$));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { Cardano, StakePoolProvider } from '@cardano-sdk/core';
2+
import { PersonalWallet, SharedWallet } from '@cardano-sdk/wallet';
3+
import { buildSharedWallets } from '../wallet/SharedWallet/ultils';
4+
import { filter, firstValueFrom, map, take } from 'rxjs';
5+
import {
6+
getEnv,
7+
getTxConfirmationEpoch,
8+
getWallet,
9+
runningAgainstLocalNetwork,
10+
submitAndConfirm,
11+
waitForEpoch,
12+
walletReady,
13+
walletVariables
14+
} from '../../src';
15+
import { isNotNil } from '@cardano-sdk/util';
16+
import { logger } from '@cardano-sdk/util-dev';
17+
import { waitForWalletStateSettle } from '../../../wallet/test/util';
18+
19+
const env = getEnv(walletVariables);
20+
21+
const submitDelegationTx = async (
22+
alice: SharedWallet,
23+
bob: SharedWallet,
24+
charlotte: SharedWallet,
25+
pool: Cardano.PoolId
26+
) => {
27+
logger.info(`Creating delegation tx at epoch #${(await firstValueFrom(alice.currentEpoch$)).epochNo}`);
28+
let tx = (await alice.createTxBuilder().delegateFirstStakeCredential(pool).build().sign()).tx;
29+
30+
tx = await bob.updateWitness({ sender: { id: 'e2e' }, tx });
31+
tx = await charlotte.updateWitness({ sender: { id: 'e2e' }, tx });
32+
await alice.submitTx(tx);
33+
34+
const { epochNo } = await firstValueFrom(alice.currentEpoch$);
35+
logger.info(`Delegation tx ${tx.id} submitted at epoch #${epochNo}`);
36+
37+
return tx;
38+
};
39+
40+
const generateTxs = async (sendingWallet: PersonalWallet, receivingWallet: SharedWallet) => {
41+
logger.info('Sending 100 txs to generate reward fees');
42+
43+
const tAdaToSend = 5_000_000n;
44+
const [{ address: receivingAddress }] = await firstValueFrom(receivingWallet.addresses$);
45+
46+
for (let i = 0; i < 100; i++) {
47+
const txBuilder = sendingWallet.createTxBuilder();
48+
const txOut = await txBuilder.buildOutput().address(receivingAddress).coin(tAdaToSend).build();
49+
const { tx: signedTx } = await txBuilder.addOutput(txOut).build().sign();
50+
await sendingWallet.submitTx(signedTx);
51+
}
52+
};
53+
54+
const buildSpendRewardTx = async (
55+
alice: SharedWallet,
56+
bob: SharedWallet,
57+
charlotte: SharedWallet,
58+
receivingWallet: PersonalWallet
59+
) => {
60+
const tAdaToSend = 5_000_000n;
61+
const [{ address: receivingAddress }] = await firstValueFrom(receivingWallet.addresses$);
62+
const txBuilder = alice.createTxBuilder();
63+
const txOut = await txBuilder.buildOutput().address(receivingAddress).coin(tAdaToSend).build();
64+
const tx = txBuilder.addOutput(txOut).build();
65+
66+
const { body } = await tx.inspect();
67+
logger.debug('Body of tx before sign');
68+
logger.debug(body);
69+
let signedTx = (await tx.sign()).tx;
70+
71+
signedTx = await bob.updateWitness({ sender: { id: 'e2e' }, tx: signedTx });
72+
signedTx = await charlotte.updateWitness({ sender: { id: 'e2e' }, tx: signedTx });
73+
74+
logger.debug('Body of tx after sign');
75+
logger.debug(signedTx.body);
76+
77+
return signedTx;
78+
};
79+
80+
const getPoolIds = async (stakePoolProvider: StakePoolProvider, count: number) => {
81+
const activePools = await stakePoolProvider.queryStakePools({
82+
filters: { pledgeMet: true, status: [Cardano.StakePoolStatus.Active] },
83+
pagination: { limit: count, startAt: 0 }
84+
});
85+
expect(activePools.totalResultCount).toBeGreaterThanOrEqual(count);
86+
const poolIds = activePools.pageResults.map(({ id }) => id);
87+
expect(poolIds.every((poolId) => poolId !== undefined)).toBeTruthy();
88+
logger.info('Wallet funds will be staked to pools:', poolIds);
89+
return poolIds;
90+
};
91+
92+
describe('delegation rewards', () => {
93+
let fundingTx: Cardano.Tx<Cardano.TxBody>;
94+
let faucetWallet: PersonalWallet;
95+
let faucetAddress: Cardano.PaymentAddress;
96+
let aliceMultiSigWallet: SharedWallet;
97+
let bobMultiSigWallet: SharedWallet;
98+
let charlotteMultiSigWallet: SharedWallet;
99+
let stakePoolProvider: StakePoolProvider;
100+
101+
const initialFunds = 10_000_000n;
102+
103+
beforeAll(async () => {
104+
jest.setTimeout(180_000);
105+
106+
({
107+
wallet: faucetWallet,
108+
providers: { stakePoolProvider }
109+
} = await getWallet({ env, logger, name: 'Sending Wallet', polling: { interval: 50 } }));
110+
111+
// Make sure the wallet has sufficient funds to run this test
112+
await walletReady(faucetWallet, initialFunds);
113+
114+
faucetAddress = (await firstValueFrom(faucetWallet.addresses$))[0].address;
115+
116+
({ aliceMultiSigWallet, bobMultiSigWallet, charlotteMultiSigWallet } = await buildSharedWallets(
117+
env,
118+
await firstValueFrom(faucetWallet.genesisParameters$),
119+
logger
120+
));
121+
122+
await Promise.all([
123+
waitForWalletStateSettle(aliceMultiSigWallet),
124+
waitForWalletStateSettle(bobMultiSigWallet),
125+
waitForWalletStateSettle(charlotteMultiSigWallet)
126+
]);
127+
128+
const [{ address: receivingAddress }] = await firstValueFrom(aliceMultiSigWallet.addresses$);
129+
130+
logger.info(`Address ${faucetAddress} will send ${initialFunds} lovelace to address ${receivingAddress}.`);
131+
132+
// Send 10 tADA to the shared wallet.
133+
const txBuilder = faucetWallet.createTxBuilder();
134+
const txOutput = await txBuilder.buildOutput().address(receivingAddress).coin(initialFunds).build();
135+
fundingTx = (await txBuilder.addOutput(txOutput).build().sign()).tx;
136+
await faucetWallet.submitTx(fundingTx);
137+
138+
logger.info(
139+
`Submitted transaction id: ${fundingTx.id}, inputs: ${JSON.stringify(
140+
fundingTx.body.inputs.map((txIn) => [txIn.txId, txIn.index])
141+
)} and outputs:${JSON.stringify(
142+
fundingTx.body.outputs.map((txOut) => [txOut.address, Number.parseInt(txOut.value.coins.toString())])
143+
)}.`
144+
);
145+
});
146+
147+
afterAll(() => {
148+
faucetWallet.shutdown();
149+
aliceMultiSigWallet.shutdown();
150+
bobMultiSigWallet.shutdown();
151+
charlotteMultiSigWallet.shutdown();
152+
faucetWallet.shutdown();
153+
});
154+
155+
it('will receive rewards for delegated tADA and can spend them', async () => {
156+
if (!(await runningAgainstLocalNetwork())) {
157+
return logger.fatal(
158+
"Skipping test 'will receive rewards for delegated tADA' as it should only run with a fast test network"
159+
);
160+
}
161+
162+
const txFoundInHistory = await firstValueFrom(
163+
aliceMultiSigWallet.transactions.history$.pipe(
164+
map((txs) => txs.find((tx) => tx.id === fundingTx.id)),
165+
filter(isNotNil),
166+
take(1)
167+
)
168+
);
169+
170+
expect(txFoundInHistory?.id).toEqual(fundingTx.id);
171+
172+
// Arrange
173+
const [poolId] = await getPoolIds(stakePoolProvider, 1);
174+
175+
// Stake and wait for reward
176+
const signedTx = await submitDelegationTx(aliceMultiSigWallet, bobMultiSigWallet, charlotteMultiSigWallet, poolId);
177+
178+
const delegationTxConfirmedAtEpoch = await getTxConfirmationEpoch(aliceMultiSigWallet, signedTx);
179+
180+
logger.info(`Delegation tx confirmed at epoch #${delegationTxConfirmedAtEpoch}`);
181+
182+
await waitForEpoch(aliceMultiSigWallet, delegationTxConfirmedAtEpoch + 2);
183+
184+
await generateTxs(faucetWallet, aliceMultiSigWallet);
185+
await waitForEpoch(aliceMultiSigWallet, delegationTxConfirmedAtEpoch + 4);
186+
187+
// Check reward
188+
await waitForWalletStateSettle(aliceMultiSigWallet);
189+
const rewards = await firstValueFrom(aliceMultiSigWallet.balance.rewardAccounts.rewards$);
190+
expect(rewards).toBeGreaterThan(0n);
191+
192+
logger.info(`Generated rewards: ${rewards} tLovelace`);
193+
194+
// Spend reward
195+
const spendRewardTx = await buildSpendRewardTx(
196+
aliceMultiSigWallet,
197+
bobMultiSigWallet,
198+
charlotteMultiSigWallet,
199+
faucetWallet
200+
);
201+
expect(spendRewardTx.body.withdrawals?.length).toBeGreaterThan(0);
202+
await submitAndConfirm(aliceMultiSigWallet, spendRewardTx);
203+
});
204+
});

0 commit comments

Comments
 (0)