@cardano-sdk/e2e 0.14.2 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,10 @@ services:
12
12
  <<: *logging
13
13
  build:
14
14
  context: ./local-network
15
+ depends_on:
16
+ # We need the file server here in order to calculate the pool metadata hashes
17
+ file-server:
18
+ condition: service_healthy
15
19
  environment:
16
20
  CARDANO_NODE_LOG_LEVEL: ${CARDANO_NODE_LOG_LEVEL:-Info}
17
21
  CARDANO_NODE_CHAINDB_LOG_LEVEL: ${CARDANO_NODE_CHAINDB_LOG_LEVEL:-Notice}
@@ -20,10 +24,7 @@ services:
20
24
  volumes:
21
25
  - ./local-network/network-files/node-sp1/:/root/network-files/node-sp1
22
26
  - ./local-network/config:/root/config
23
- depends_on:
24
- # We need the file server here in order to calculate the pool metadata hashes
25
- file-server:
26
- condition: service_healthy
27
+ - sdk-ipc:/sdk-ipc
27
28
 
28
29
  file-server:
29
30
  <<: *logging
@@ -39,12 +40,15 @@ services:
39
40
  timeout: 10s
40
41
 
41
42
  cardano-node-ogmios:
43
+ entrypoint: [ "/tini", "-g", "--", "/scripts/cardano-node-ogmios.sh" ]
42
44
  image: cardanosolutions/cardano-node-ogmios:v${OGMIOS_VERSION:-5.6.0}_${CARDANO_NODE_VERSION:-1.35.5}
43
45
  depends_on:
44
46
  local-testnet:
45
47
  condition: service_healthy
46
48
  volumes:
47
49
  - ./local-network/config/network:/config
50
+ - ./local-network/scripts:/scripts
51
+ - sdk-ipc:/sdk-ipc
48
52
 
49
53
  cardano-db-sync:
50
54
  depends_on:
@@ -72,6 +76,12 @@ services:
72
76
  condition: service_healthy
73
77
  restart: on-failure
74
78
 
79
+ handle-projector:
80
+ environment:
81
+ HANDLE_POLICY_IDS_FILE: /sdk-ipc/handle_policy_ids
82
+ volumes:
83
+ - sdk-ipc:/sdk-ipc
84
+
75
85
  provider-server:
76
86
  environment:
77
87
  TOKEN_METADATA_SERVER_URL: stub://
@@ -79,4 +89,10 @@ services:
79
89
  - ./local-network/config/network:/config
80
90
 
81
91
  volumes:
92
+ sdk-ipc:
93
+ driver: local
94
+ driver_opts:
95
+ device: ./local-network/sdk-ipc
96
+ o: bind
97
+ type: none
82
98
  wallet-db:
@@ -0,0 +1,13 @@
1
+ #!/bin/bash
2
+
3
+ # Simple scripts which overrides the original cardano-node-ogmios.sh file from the
4
+ # cardano-node-ogmios docker image.
5
+
6
+ # Used to support the e2e test to check the projector is able to
7
+ # connect / reconnect to the ogmios server.
8
+
9
+ # If the test set the file, wait for its removal before starting the container
10
+ while [ -f /sdk-ipc/prevent_ogmios ] ; do sleep 10 ; done
11
+
12
+ # Start the cardano-node-ogmios as normal
13
+ /root/cardano-node-ogmios.sh
@@ -25,6 +25,7 @@ cat >network-files/utxo-keys/handles-metadata.json <<EOL
25
25
  }}
26
26
  EOL
27
27
 
28
+ echo $policyid > /sdk-ipc/handle_policy_ids
28
29
 
29
30
  addr=$(cardano-cli address build --payment-verification-key-file network-files/utxo-keys/utxo1.vkey --testnet-magic 888)
30
31
  faucetAddr="addr_test1qqen0wpmhg7fhkus45lyv4wju26cecgu6avplrnm6dgvuk6qel5hu3u3q0fht53ly97yx95hkt56j37ch07pesf6s4pqh5gd4e"
@@ -0,0 +1,5 @@
1
+ ## Description
2
+
3
+ Empty directory used as IPC between SDK containers and with the extern.
4
+
5
+ Used for files required by tests or to share ADA handle policy id between containers, etc...
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cardano-sdk/e2e",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "End to end tests for the cardano-js-sdk packages.",
5
5
  "engines": {
6
6
  "node": ">=16.0"
@@ -81,18 +81,18 @@
81
81
  "dependencies": {
82
82
  "@cardano-foundation/ledgerjs-hw-app-cardano": "^6.0.0",
83
83
  "@cardano-ogmios/client": "5.6.0",
84
- "@cardano-sdk/cardano-services": "~0.13.2",
85
- "@cardano-sdk/cardano-services-client": "~0.9.8",
86
- "@cardano-sdk/core": "~0.14.1",
87
- "@cardano-sdk/crypto": "~0.1.6",
88
- "@cardano-sdk/hardware-ledger": "~0.2.7",
89
- "@cardano-sdk/key-management": "~0.7.6",
90
- "@cardano-sdk/ogmios": "~0.12.4",
91
- "@cardano-sdk/tx-construction": "~0.7.2",
92
- "@cardano-sdk/util": "~0.11.0",
93
- "@cardano-sdk/util-dev": "~0.13.2",
94
- "@cardano-sdk/util-rxjs": "~0.4.15",
95
- "@cardano-sdk/wallet": "~0.16.2",
84
+ "@cardano-sdk/cardano-services": "~0.14.0",
85
+ "@cardano-sdk/cardano-services-client": "~0.10.0",
86
+ "@cardano-sdk/core": "~0.15.1",
87
+ "@cardano-sdk/crypto": "~0.1.7",
88
+ "@cardano-sdk/hardware-ledger": "~0.2.9",
89
+ "@cardano-sdk/key-management": "~0.7.8",
90
+ "@cardano-sdk/ogmios": "~0.12.6",
91
+ "@cardano-sdk/tx-construction": "~0.8.1",
92
+ "@cardano-sdk/util": "~0.12.0",
93
+ "@cardano-sdk/util-dev": "~0.13.4",
94
+ "@cardano-sdk/util-rxjs": "~0.5.0",
95
+ "@cardano-sdk/wallet": "~0.17.0",
96
96
  "@vespaiach/axios-fetch-adapter": "^0.3.0",
97
97
  "axios": "^0.27.2",
98
98
  "bunyan": "^1.8.15",
@@ -122,16 +122,17 @@
122
122
  "@babel/core": "^7.18.2",
123
123
  "@babel/preset-env": "^7.18.2",
124
124
  "@babel/preset-typescript": "^7.17.12",
125
- "@cardano-sdk/dapp-connector": "~0.9.6",
126
- "@cardano-sdk/projection": "~0.6.8",
127
- "@cardano-sdk/projection-typeorm": "~0.3.4",
128
- "@cardano-sdk/web-extension": "~0.12.4",
125
+ "@cardano-sdk/dapp-connector": "~0.9.8",
126
+ "@cardano-sdk/projection": "~0.6.10",
127
+ "@cardano-sdk/projection-typeorm": "~0.3.6",
128
+ "@cardano-sdk/web-extension": "~0.13.1",
129
129
  "@dcspark/cardano-multiplatform-lib-browser": "^3.1.1",
130
130
  "@emurgo/cardano-message-signing-asmjs": "^1.0.1",
131
131
  "@types/bunyan": "^1.8.8",
132
132
  "@types/chalk": "^2.2.0",
133
133
  "@types/convict": "^6.1.2",
134
134
  "@types/delay": "^3.1.0",
135
+ "@types/dockerode": "^3.3.8",
135
136
  "@types/jest": "^28.1.2",
136
137
  "@types/lodash": "^4.14.182",
137
138
  "@types/ora": "^3.2.0",
@@ -146,7 +147,7 @@
146
147
  "babel-loader": "^8.2.5",
147
148
  "blake2b-no-wasm": "2.1.4",
148
149
  "buffer": "^6.0.3",
149
- "chromedriver": "^112.0.0",
150
+ "chromedriver": "^114.0.0",
150
151
  "copy-webpack-plugin": "^10.2.4",
151
152
  "eslint": "^7.32.0",
152
153
  "events": "^3.3.0",
@@ -173,5 +174,5 @@
173
174
  "webpack-cli": "^4.9.2",
174
175
  "webpack-merge": "^5.8.0"
175
176
  },
176
- "gitHead": "badfa920af582c12079ce55bf74ba7337d9cc3cb"
177
+ "gitHead": "46029ae65c25ce6cfef394249c7ff5a32259757f"
177
178
  }
package/src/factories.ts CHANGED
@@ -100,7 +100,13 @@ assetProviderFactory.register(HTTP_PROVIDER, async (params: any, logger: Logger)
100
100
  if (params.baseUrl === undefined) throw new Error(`${assetInfoHttpProvider.name}: ${MISSING_URL_PARAM}`);
101
101
 
102
102
  return new Promise<AssetProvider>(async (resolve) => {
103
- resolve(assetInfoHttpProvider({ adapter: customHttpFetchAdapter, baseUrl: params.baseUrl, logger }));
103
+ resolve(
104
+ assetInfoHttpProvider({
105
+ adapter: customHttpFetchAdapter,
106
+ baseUrl: params.baseUrl,
107
+ logger
108
+ })
109
+ );
104
110
  });
105
111
  });
106
112
 
@@ -47,7 +47,7 @@ describe('cache invalidation', () => {
47
47
 
48
48
  afterAll(() => wallet1.wallet.shutdown());
49
49
 
50
- test('cache is invalidated on epoch rollover', async () => {
50
+ test.skip('cache is invalidated on epoch rollover', async () => {
51
51
  const wallet = wallet1.wallet;
52
52
 
53
53
  await walletReady(wallet);
@@ -5,10 +5,8 @@ import {
5
5
  getEnv,
6
6
  getTxConfirmationEpoch,
7
7
  getWallet,
8
- requestCoins,
9
8
  runningAgainstLocalNetwork,
10
9
  submitAndConfirm,
11
- transferCoins,
12
10
  waitForEpoch,
13
11
  walletVariables
14
12
  } from '../../src';
@@ -24,9 +22,6 @@ describe('delegation rewards', () => {
24
22
  let wallet2: PersonalWallet;
25
23
 
26
24
  const initializeWallets = async () => {
27
- const amountFromFaucet = 100_000_000_000n;
28
- const tAdaToSend = 50_000_000n;
29
-
30
25
  ({ wallet: wallet1, providers } = await getWallet({
31
26
  env,
32
27
  logger,
@@ -35,9 +30,6 @@ describe('delegation rewards', () => {
35
30
  }));
36
31
  ({ wallet: wallet2 } = await getWallet({ env, logger, name: 'Receiving Wallet', polling: { interval: 50 } }));
37
32
 
38
- await requestCoins({ coins: amountFromFaucet, wallet: wallet1 });
39
- await transferCoins({ coins: tAdaToSend, fromWallet: wallet1, toWallet: wallet2 });
40
-
41
33
  await waitForWalletStateSettle(wallet1);
42
34
  await waitForWalletStateSettle(wallet2);
43
35
  };
@@ -70,7 +62,13 @@ describe('delegation rewards', () => {
70
62
 
71
63
  const submitDelegationTx = async () => {
72
64
  logger.info(`Creating delegation tx at epoch #${(await firstValueFrom(wallet1.currentEpoch$)).epochNo}`);
73
- const { tx: signedTx } = await wallet1.createTxBuilder().delegate(poolId).build().sign();
65
+ const { tx: signedTx } = await wallet1
66
+ .createTxBuilder()
67
+ .delegatePortfolio({
68
+ pools: [{ id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolId)), weight: 1 }]
69
+ })
70
+ .build()
71
+ .sign();
74
72
  await wallet1.submitTx(signedTx);
75
73
  const { epochNo } = await firstValueFrom(wallet1.currentEpoch$);
76
74
  logger.info(`Delegation tx ${signedTx.id} submitted at epoch #${epochNo}`);
@@ -0,0 +1,132 @@
1
+ import * as envalid from 'envalid';
2
+ import { DockerUtil } from '@cardano-sdk/util-dev';
3
+ import { open, rm } from 'fs/promises';
4
+ import Dockerode from 'dockerode';
5
+ import axios, { AxiosResponse } from 'axios';
6
+ import delay from 'delay';
7
+ import path from 'path';
8
+
9
+ const preventOgmiosStartFile = path.join(__dirname, '..', '..', 'local-network', 'sdk-ipc', 'prevent_ogmios');
10
+
11
+ const docker = new Dockerode();
12
+
13
+ const ogmiosContainer = docker.getContainer('local-network-e2e-cardano-node-ogmios-1');
14
+ const stakePoolProjectorContainer = docker.getContainer('local-network-e2e-stake-pool-projector-1');
15
+
16
+ const createPrevent = async () => {
17
+ const file = await open(preventOgmiosStartFile, 'a');
18
+
19
+ await file.close();
20
+ };
21
+
22
+ const removePrevent = () => rm(preventOgmiosStartFile, { force: true });
23
+
24
+ const killContainer = async (container: DockerUtil.Docker.Container) => {
25
+ // This waits only for the signal to be sent
26
+ await DockerUtil.containerExec(container, ['kill', '1']);
27
+ // Let's wait one more second to ensure the container was shut down
28
+ await delay(1000);
29
+ };
30
+
31
+ describe('projector ogmios connection', () => {
32
+ let stakePoolProjectorUrl: string;
33
+
34
+ const fetchProjectorHealth = async () => {
35
+ let result: AxiosResponse;
36
+
37
+ try {
38
+ result = await axios.post(stakePoolProjectorUrl, {});
39
+ } catch (error) {
40
+ if (axios.isAxiosError(error)) throw new Error(error.message);
41
+
42
+ throw error;
43
+ }
44
+
45
+ return result;
46
+ };
47
+
48
+ const checkProjector = async () => {
49
+ const health = await fetchProjectorHealth();
50
+
51
+ if (!health.data.ok) throw new Error('Projector not healthy');
52
+
53
+ const { blockNo } = health.data.services[0].projectedTip;
54
+ const start = Date.now();
55
+
56
+ // Wait for another block to be projected to ensure projector is working correctly
57
+ while ((await fetchProjectorHealth()).data.services[0].projectedTip.blockNo === blockNo) {
58
+ if (Date.now() - start > 60_000) throw new Error('The projector is not projecting new blocks');
59
+
60
+ await delay(100);
61
+ }
62
+ };
63
+
64
+ const waitProjector = async (skipOkCheck = false, tolerateConnectionErrors = false) => {
65
+ const start = Date.now();
66
+ let projectorReady = false;
67
+
68
+ do {
69
+ try {
70
+ const health = await fetchProjectorHealth();
71
+
72
+ if ('ok' in health.data && (skipOkCheck || health.data.ok)) projectorReady = true;
73
+ } catch (error) {
74
+ if (error && !tolerateConnectionErrors) throw error;
75
+ }
76
+
77
+ if (Date.now() - start > 60_000) throw new Error("The projector can't get ready");
78
+ } while (projectorReady === false);
79
+ };
80
+
81
+ beforeAll(() => {
82
+ const env = envalid.cleanEnv(process.env, { STAKE_POOL_PROJECTOR_URL: envalid.url() });
83
+
84
+ stakePoolProjectorUrl = `${env.STAKE_POOL_PROJECTOR_URL}health`;
85
+ });
86
+
87
+ beforeEach(async () => {
88
+ await removePrevent();
89
+ await checkProjector();
90
+ });
91
+
92
+ afterEach(() => removePrevent());
93
+
94
+ it.skip('projector reconnects after a short delay', async () => {
95
+ await killContainer(ogmiosContainer);
96
+ await waitProjector();
97
+ // This throws because is not able to complete HTTP request
98
+ await checkProjector();
99
+ });
100
+
101
+ // Once the problem which prevent the original test to work is solved
102
+ // the original test can be un-skipped and this one can be removed
103
+ it('projector reconnects after a short delay - alternative', async () => {
104
+ await killContainer(ogmiosContainer);
105
+ // Add a 10" sleep as a quick workaround for HTTP connection issue
106
+ await delay(10_000);
107
+ await waitProjector();
108
+ await checkProjector();
109
+ });
110
+
111
+ it.skip('projector reconnects after a long delay', async () => {
112
+ await createPrevent();
113
+ await killContainer(ogmiosContainer);
114
+ await delay(120_000);
115
+ await removePrevent();
116
+ await waitProjector();
117
+ await checkProjector();
118
+ });
119
+
120
+ it('projector connects to a later started ogmios', async () => {
121
+ await createPrevent();
122
+ await killContainer(ogmiosContainer);
123
+ await killContainer(stakePoolProjectorContainer);
124
+ await waitProjector(true, true);
125
+ await removePrevent();
126
+ // Once the 'projector reconnects after a short delay' problem is solved
127
+ // also remove following line and uncomment next one
128
+ await waitProjector(false, true);
129
+ // await waitProjector();
130
+ await checkProjector();
131
+ });
132
+ });
@@ -113,7 +113,7 @@ describe('PersonalWallet/delegation', () => {
113
113
 
114
114
  const { tx } = await txBuilder
115
115
  .addOutput(await txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).build())
116
- .delegate(poolId)
116
+ .delegatePortfolio({ pools: [{ id: Cardano.PoolIdHex(Cardano.PoolId.toKeyHash(poolId)), weight: 1 }] })
117
117
  .build()
118
118
  .sign();
119
119
  await sourceWallet.submitTx(tx);
@@ -163,7 +163,7 @@ describe('PersonalWallet/delegation', () => {
163
163
  }
164
164
 
165
165
  // Make a 2nd tx with key de-registration
166
- const { tx: txDeregisterSigned } = await sourceWallet.createTxBuilder().delegate().build().sign();
166
+ const { tx: txDeregisterSigned } = await sourceWallet.createTxBuilder().delegatePortfolio(null).build().sign();
167
167
  await sourceWallet.submitTx(txDeregisterSigned);
168
168
  await waitForTx(sourceWallet, txDeregisterSigned.id);
169
169
  const tx2ConfirmedState = await getWalletStateSnapshot(sourceWallet);
@@ -1,4 +1,3 @@
1
- import { AddressType } from '@cardano-sdk/key-management';
2
1
  import { Cardano } from '@cardano-sdk/core';
3
2
  import { DelegatedStake, PersonalWallet, createUtxoBalanceByAddressTracker } from '@cardano-sdk/wallet';
4
3
  import { MINUTE, firstValueFromTimed, getWallet, submitAndConfirm, walletReady } from '../../../src';
@@ -6,29 +5,17 @@ import { Observable, filter, firstValueFrom, map, tap } from 'rxjs';
6
5
  import { Percent } from '@cardano-sdk/util';
7
6
  import { createLogger } from '@cardano-sdk/util-dev';
8
7
  import { getEnv, walletVariables } from '../../../src/environment';
9
- import delay from 'delay';
10
8
 
11
9
  const env = getEnv(walletVariables);
12
10
  const logger = createLogger();
13
11
  const TEST_FUNDS = 1_000_000_000n;
12
+ const POOLS_COUNT = 5;
14
13
  const distributionMessage = 'ObservableWallet.delegation.distribution$:';
15
14
 
16
- const deriveStakeKeys = async (wallet: PersonalWallet) => {
17
- await walletReady(wallet, 0n);
18
- // Add 4 new addresses with different stake keys.
19
- for (let i = 1; i < 5; ++i) {
20
- await wallet.keyAgent.deriveAddress({ index: 0, type: AddressType.External }, i);
21
- }
22
- // Allow status tracker to change status with debounce.
23
- // Otherwise the updates are be debounced and next calls find the wallet ready before it had a chance to update the status.
24
- await delay(2);
25
- };
26
-
27
15
  /** Distribute the wallet funds evenly across all its addresses */
28
- const distributeFunds = async (wallet: PersonalWallet) => {
16
+ const fundWallet = async (wallet: PersonalWallet) => {
29
17
  await walletReady(wallet, 0n);
30
18
  const addresses = await firstValueFrom(wallet.addresses$);
31
- expect(addresses.length).toBeGreaterThan(1);
32
19
 
33
20
  // Check that we have enough funds. Otherwise, fund it from wallet account at index 0
34
21
  let { coins: totalCoins } = await firstValueFrom(wallet.balance.utxo.available$);
@@ -50,22 +37,6 @@ const distributeFunds = async (wallet: PersonalWallet) => {
50
37
  await walletReady(wallet);
51
38
  totalCoins = (await firstValueFrom(wallet.balance.utxo.available$)).coins;
52
39
  }
53
-
54
- const coinsPerAddress = totalCoins / BigInt(addresses.length);
55
-
56
- const txBuilder = wallet.createTxBuilder();
57
-
58
- logger.info(`Sending ${coinsPerAddress} to the ${addresses.length - 1} derived addresses`);
59
- // The first one was generated when the wallet was created.
60
- for (let i = 1; i < addresses.length; ++i) {
61
- const derivedAddress = addresses[i];
62
- logger.info('Funding', derivedAddress.address, coinsPerAddress);
63
- logger.info(derivedAddress.rewardAccount);
64
- txBuilder.addOutput(txBuilder.buildOutput().address(derivedAddress.address).coin(coinsPerAddress).toTxOut());
65
- }
66
-
67
- const { tx: signedTx } = await txBuilder.build().sign();
68
- await submitAndConfirm(wallet, signedTx);
69
40
  };
70
41
 
71
42
  /** await for rewardAccounts$ to be registered, unregistered, as defined in states */
@@ -97,7 +68,7 @@ const deregisterAllStakeKeys = async (wallet: PersonalWallet): Promise<void> =>
97
68
  } catch {
98
69
  // Some stake keys are registered. Deregister them
99
70
  const txBuilder = wallet.createTxBuilder();
100
- txBuilder.delegate();
71
+ txBuilder.delegatePortfolio(null);
101
72
  const { tx: deregTx } = await txBuilder.build().sign();
102
73
  await submitAndConfirm(wallet, deregTx);
103
74
 
@@ -109,36 +80,44 @@ const deregisterAllStakeKeys = async (wallet: PersonalWallet): Promise<void> =>
109
80
  }
110
81
  };
111
82
 
112
- const getPoolIds = async (wallet: PersonalWallet, count: number): Promise<Cardano.StakePool[]> => {
83
+ const getPoolIds = async (wallet: PersonalWallet): Promise<Cardano.StakePool[]> => {
113
84
  const activePools = await wallet.stakePoolProvider.queryStakePools({
114
85
  filters: { status: [Cardano.StakePoolStatus.Active] },
115
- pagination: { limit: count, startAt: 0 }
86
+ pagination: { limit: POOLS_COUNT, startAt: 0 }
116
87
  });
117
- expect(activePools.pageResults.length).toBeGreaterThanOrEqual(count);
118
- return Array.from({ length: count }).map((_, index) => activePools.pageResults[index]);
88
+ expect(activePools.pageResults.length).toBeGreaterThanOrEqual(POOLS_COUNT);
89
+ return Array.from({ length: POOLS_COUNT }).map((_, index) => activePools.pageResults[index]);
119
90
  };
120
91
 
121
- const delegateToMultiplePools = async (wallet: PersonalWallet) => {
122
- // Delegating to multiple pools should be added in TxBuilder. Doing it manually for now.
123
- // Prepare stakeKey registration certificates
124
- const rewardAccounts = await firstValueFrom(wallet.delegation.rewardAccounts$);
125
- const stakeKeyRegCertificates = rewardAccounts.map(({ address }) => Cardano.createStakeKeyRegistrationCert(address));
126
-
127
- const poolIds = await getPoolIds(wallet, rewardAccounts.length);
128
- const delegationCertificates = rewardAccounts.map(({ address }, index) =>
129
- Cardano.createDelegationCert(address, poolIds[index].id)
130
- );
92
+ /** Delegate to unique POOLS_COUNT pools. Use even distribution as default. */
93
+ const delegateToMultiplePools = async (
94
+ wallet: PersonalWallet,
95
+ weights = Array.from({ length: POOLS_COUNT }).map(() => 1)
96
+ ) => {
97
+ const poolIds = await getPoolIds(wallet);
98
+ const portfolio: Pick<Cardano.Cip17DelegationPortfolio, 'pools'> = {
99
+ pools: poolIds.map(({ hexId: id }, idx) => ({ id, weight: weights[idx] }))
100
+ };
101
+ logger.debug('Delegating portfolio', portfolio);
102
+
103
+ const { tx } = await wallet.createTxBuilder().delegatePortfolio(portfolio).build().sign();
104
+ await submitAndConfirm(wallet, tx);
105
+ return poolIds;
106
+ };
131
107
 
132
- logger.debug(
133
- `Delegating to pools ${poolIds.map(({ id }) => id)} and registering ${stakeKeyRegCertificates.length} stake keys`
108
+ const delegateAllToSinglePool = async (wallet: PersonalWallet): Promise<void> => {
109
+ // This is a negative testcase, simulating an HD wallet that has multiple stake keys delegated
110
+ // to the same stake pool. txBuilder.delegatePortfolio does not support this scenario.
111
+ const [{ id: poolId }] = await getPoolIds(wallet);
112
+ const txBuilder = wallet.createTxBuilder();
113
+ const rewardAccounts = await firstValueFrom(wallet.delegation.rewardAccounts$);
114
+ txBuilder.partialTxBody.certificates = rewardAccounts.map(({ address }) =>
115
+ Cardano.createDelegationCert(address, poolId)
134
116
  );
135
117
 
136
- const txBuilder = wallet.createTxBuilder();
137
- // Artificially add the certificates in TxBuilder. An api improvement will make the UX better
138
- txBuilder.partialTxBody.certificates = [...stakeKeyRegCertificates, ...delegationCertificates];
118
+ logger.debug(`Delegating all stake keys to pool ${poolId}`);
139
119
  const { tx } = await txBuilder.build().sign();
140
120
  await submitAndConfirm(wallet, tx);
141
- return poolIds;
142
121
  };
143
122
 
144
123
  describe('PersonalWallet/delegationDistribution', () => {
@@ -146,9 +125,8 @@ describe('PersonalWallet/delegationDistribution', () => {
146
125
 
147
126
  beforeAll(async () => {
148
127
  wallet = (await getWallet({ env, idx: 3, logger, name: 'Wallet', polling: { interval: 50 } })).wallet;
149
- await deriveStakeKeys(wallet);
128
+ await fundWallet(wallet);
150
129
  await deregisterAllStakeKeys(wallet);
151
- await distributeFunds(wallet);
152
130
  });
153
131
 
154
132
  afterAll(() => {
@@ -158,19 +136,16 @@ describe('PersonalWallet/delegationDistribution', () => {
158
136
  it('reports observable wallet multi delegation as delegationDistribution by pool', async () => {
159
137
  await walletReady(wallet);
160
138
 
161
- const walletAddresses = await firstValueFromTimed(wallet.addresses$);
162
- const rewardAccounts = await firstValueFrom(wallet.delegation.rewardAccounts$);
163
-
164
- expect(rewardAccounts.length).toBe(5);
165
-
166
139
  // No stake distribution initially
167
140
  const delegationDistribution = await firstValueFrom(wallet.delegation.distribution$);
168
141
  logger.info('Empty delegation distribution initially');
169
142
  expect(delegationDistribution).toEqual(new Map());
170
143
 
171
144
  const poolIds = await delegateToMultiplePools(wallet);
172
- // Redistribute the funds because delegation costs send change to the first account, messing up the uniform distribution
173
- await distributeFunds(wallet);
145
+ const walletAddresses = await firstValueFromTimed(wallet.addresses$);
146
+ const rewardAccounts = await firstValueFrom(wallet.delegation.rewardAccounts$);
147
+
148
+ expect(rewardAccounts.length).toBe(POOLS_COUNT);
174
149
 
175
150
  // Check that reward addresses were delegated
176
151
  await walletReady(wallet);
@@ -204,20 +179,11 @@ describe('PersonalWallet/delegationDistribution', () => {
204
179
 
205
180
  expect([...actualDelegationDistribution.values()]).toEqual(expectedDelegationDistribution);
206
181
 
207
- // Send all coins to the last address. Check that stake distribution is 100 for that address and 0 for the rest
208
- const { coins: totalCoins } = await firstValueFrom(wallet.balance.utxo.total$);
209
- let txBuilder = wallet.createTxBuilder();
210
- const { tx: txMoveFunds } = await txBuilder
211
- .addOutput(
212
- txBuilder
213
- .buildOutput()
214
- .address(walletAddresses[walletAddresses.length - 1].address)
215
- .coin(totalCoins - 2_000_000n) // leave some behind for fees
216
- .toTxOut()
217
- )
218
- .build()
219
- .sign();
220
- await submitAndConfirm(wallet, txMoveFunds);
182
+ // Delegate so that last address has all funds
183
+ await delegateToMultiplePools(
184
+ wallet,
185
+ Array.from({ length: POOLS_COUNT }).map((_, idx) => (POOLS_COUNT === idx + 1 ? 1 : 0))
186
+ );
221
187
 
222
188
  let simplifiedDelegationDistribution: Partial<DelegatedStake>[] = await firstValueFrom(
223
189
  wallet.delegation.distribution$.pipe(
@@ -246,9 +212,7 @@ describe('PersonalWallet/delegationDistribution', () => {
246
212
  );
247
213
 
248
214
  // Delegate all reward accounts to the same pool. delegationDistribution$ should have 1 entry with 100% distribution
249
- txBuilder = wallet.createTxBuilder();
250
- const { tx: txDelegateTo1Pool } = await txBuilder.delegate(poolIds[0].id).build().sign();
251
- await submitAndConfirm(wallet, txDelegateTo1Pool);
215
+ await delegateAllToSinglePool(wallet);
252
216
  simplifiedDelegationDistribution = await firstValueFrom(
253
217
  wallet.delegation.distribution$.pipe(
254
218
  tap((distribution) => {
@@ -68,7 +68,7 @@ describe('Ada handle', () => {
68
68
  let policyScript: Cardano.NativeScript;
69
69
  let assetIds: Cardano.AssetId[];
70
70
 
71
- const assetNames = ['6861646e6c6531', '6861646e6c6532'];
71
+ const assetNames = ['68616e646c6531', '68616e646c6532'];
72
72
  let walletAddress: Cardano.PaymentAddress;
73
73
  const coins = 2_000_000n; // number of coins to use in each transaction
74
74
 
@@ -1,15 +1,30 @@
1
1
  // Expose any additional services to be shared with UIs
2
- import { BackgroundServices, adaPriceProperties, logger } from '../util';
2
+ import { BackgroundServices, adaPriceProperties, env, logger } from '../util';
3
+ import { Cardano } from '@cardano-sdk/core';
3
4
  import { adaPriceServiceChannel, walletName } from '../const';
4
5
  import { authenticator } from './authenticator';
5
6
  import { exposeApi, exposeSupplyDistributionTracker } from '@cardano-sdk/web-extension';
6
7
  import { of } from 'rxjs';
7
8
  import { runtime } from 'webextension-polyfill';
9
+ import { stakePoolProviderFactory } from '../../../../src';
8
10
  import { supplyDistributionTrackerReady } from './supplyDistributionTracker';
9
11
 
10
12
  const priceService: BackgroundServices = {
11
13
  adaUsd$: of(2.99),
12
- clearAllowList: authenticator.clear.bind(authenticator)
14
+ clearAllowList: authenticator.clear.bind(authenticator),
15
+ getPoolIds: async (count: number): Promise<Cardano.StakePool[]> => {
16
+ const stakePoolProvider = await stakePoolProviderFactory.create(
17
+ env.STAKE_POOL_PROVIDER,
18
+ env.STAKE_POOL_PROVIDER_PARAMS,
19
+ logger
20
+ );
21
+
22
+ const activePools = await stakePoolProvider.queryStakePools({
23
+ filters: { status: [Cardano.StakePoolStatus.Active] },
24
+ pagination: { limit: count, startAt: 0 }
25
+ });
26
+ return activePools.pageResults.slice(0, count);
27
+ }
13
28
  };
14
29
 
15
30
  exposeApi(
@@ -27,6 +27,17 @@
27
27
  <div>Stake Address: <span id="stakeAddress">-</span></div>
28
28
  <div>Balance: <span id="balance">-</span></div>
29
29
  <div>Network-wide staked: <span id="supplyDistribution">-</span></div>
30
+ <div id="multiDelegation">
31
+ <div class="distribution">
32
+ <h3>Delegation distribution:</h3>
33
+ <ul></ul>
34
+ </div>
35
+ <div class="delegate">
36
+ <p>Available Pools: <span class="pools"></span></p>
37
+ <button>Delegate to multiple pools</button>
38
+ <p class="delegateTxId"></p>
39
+ </div>
40
+ </div>
30
41
  <button id="buildAndSignTx">Build & Sign TX</button>
31
42
  <div>Signature: <span id="signature">-</span></div>
32
43
  <script src="ui.js"></script>