@effectstream/midnight-contracts 0.104.1 → 0.200.2
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.
- package/README.md +3 -3
- package/package.json +40 -22
- package/src/build-wallet.ts +3 -3
- package/src/deploy-phased.ts +3 -3
- package/src/deploy.ts +7 -7
- package/src/get-wallet-info-cli.ts +21 -34
- package/src/get-wallet-info.ts +232 -85
- package/src/midnight-env.ts +17 -19
- package/src/mod.ts +4 -0
- package/src/providers.ts +27 -12
- package/src/types.ts +6 -5
- package/test/integration-rc-stack.ts +223 -0
- package/test/migration.test.ts +219 -0
- package/test/migration.typecheck.ts +27 -0
- package/tsconfig.json +8 -0
package/src/providers.ts
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
5
|
import { Buffer } from "node:buffer";
|
|
6
|
-
import
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type { ZswapSecretKeys, DustSecretKey, CoinPublicKey, EncPublicKey, FinalizedTransaction, TransactionId } from "@midnightntwrk/ledger-v9";
|
|
7
8
|
import { httpClientProofProvider } from "@midnight-ntwrk/midnight-js-http-client-proof-provider";
|
|
8
9
|
import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
|
|
9
10
|
import { levelPrivateStateProvider } from "@midnight-ntwrk/midnight-js-level-private-state-provider";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
NodeZkConfigProvider,
|
|
13
|
+
nodeZkConfigRegistry,
|
|
14
|
+
} from "@midnight-ntwrk/midnight-js-node-zk-config-provider";
|
|
11
15
|
import type { WalletProvider, MidnightProvider, MidnightProviders, UnboundTransaction } from "@midnight-ntwrk/midnight-js-types";
|
|
12
16
|
import type { WalletFacade } from "@midnightntwrk/wallet-sdk-facade";
|
|
13
17
|
import type { NetworkUrls } from "./types.ts";
|
|
@@ -27,7 +31,7 @@ function createTtl(): Date {
|
|
|
27
31
|
* Create wallet and midnight provider adapter for WalletFacade
|
|
28
32
|
*
|
|
29
33
|
* Implements the WalletProvider and MidnightProvider interfaces
|
|
30
|
-
* as defined in @midnight-ntwrk/midnight-js-types
|
|
34
|
+
* as defined in @midnight-ntwrk/midnight-js-types v5.
|
|
31
35
|
*/
|
|
32
36
|
function createWalletAndMidnightProvider(
|
|
33
37
|
wallet: WalletFacade,
|
|
@@ -51,13 +55,15 @@ function createWalletAndMidnightProvider(
|
|
|
51
55
|
ttl?: Date
|
|
52
56
|
): Promise<FinalizedTransaction> {
|
|
53
57
|
console.log("✅ Balancing transaction", tx);
|
|
54
|
-
const
|
|
55
|
-
const finalizedTransactionRecipe = await wallet.balanceFinalizedTransaction(bound, {
|
|
58
|
+
const unboundTransactionRecipe = await wallet.balanceUnboundTransaction(tx, {
|
|
56
59
|
shieldedSecretKeys: zswapSecretKeys,
|
|
57
60
|
dustSecretKey: dustSecretKey,
|
|
58
61
|
}, { ttl: ttl ?? createTtl() } );
|
|
59
|
-
const
|
|
60
|
-
|
|
62
|
+
const signedRecipe = await wallet.signRecipe(
|
|
63
|
+
unboundTransactionRecipe,
|
|
64
|
+
(payload) => unshieldedKeystore.signDataAsync(payload),
|
|
65
|
+
);
|
|
66
|
+
return wallet.finalizeRecipe(signedRecipe);
|
|
61
67
|
},
|
|
62
68
|
submitTx(tx: FinalizedTransaction): Promise<TransactionId> {
|
|
63
69
|
console.log("✅ Submitting transaction", tx);
|
|
@@ -69,7 +75,7 @@ function createWalletAndMidnightProvider(
|
|
|
69
75
|
/**
|
|
70
76
|
* Configure all providers needed for contract deployment
|
|
71
77
|
*/
|
|
72
|
-
export function configureMidnightNodeProviders(
|
|
78
|
+
export async function configureMidnightNodeProviders(
|
|
73
79
|
wallet: WalletFacade,
|
|
74
80
|
zswapSecretKeys: ZswapSecretKeys,
|
|
75
81
|
walletZswapSecretKeys: ZswapSecretKeys,
|
|
@@ -79,7 +85,7 @@ function createWalletAndMidnightProvider(
|
|
|
79
85
|
privateStateStoreName: string,
|
|
80
86
|
zkConfigPath: string,
|
|
81
87
|
unshieldedKeystore: UnshieldedKeystore
|
|
82
|
-
): MidnightProviders {
|
|
88
|
+
): Promise<MidnightProviders> {
|
|
83
89
|
const signingKeyStoreName = `${privateStateStoreName}-signing-keys`;
|
|
84
90
|
const walletAndMidnightProvider = createWalletAndMidnightProvider(
|
|
85
91
|
wallet,
|
|
@@ -89,7 +95,13 @@ function createWalletAndMidnightProvider(
|
|
|
89
95
|
walletDustSecretKey,
|
|
90
96
|
unshieldedKeystore
|
|
91
97
|
);
|
|
92
|
-
const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath
|
|
98
|
+
const zkConfigProvider = new NodeZkConfigProvider(zkConfigPath, {
|
|
99
|
+
verify: "require",
|
|
100
|
+
});
|
|
101
|
+
// Midnight.js 5 resolves proof artifacts through a registry so transactions
|
|
102
|
+
// with cross-contract calls can select bundles by verifier key. Search from
|
|
103
|
+
// the managed-artifact parent to include sibling compiled contracts.
|
|
104
|
+
const zkConfigRegistry = await nodeZkConfigRegistry(path.dirname(zkConfigPath));
|
|
93
105
|
return {
|
|
94
106
|
// For deployment, we use full private state config because we may need to verify
|
|
95
107
|
// the deployed contract state. For batcher/transaction submission use cases,
|
|
@@ -109,8 +121,11 @@ function createWalletAndMidnightProvider(
|
|
|
109
121
|
networkUrls.indexerWS
|
|
110
122
|
),
|
|
111
123
|
zkConfigProvider,
|
|
112
|
-
proofProvider: httpClientProofProvider(
|
|
124
|
+
proofProvider: httpClientProofProvider(
|
|
125
|
+
networkUrls.proofServer,
|
|
126
|
+
zkConfigRegistry,
|
|
127
|
+
),
|
|
113
128
|
walletProvider: walletAndMidnightProvider,
|
|
114
129
|
midnightProvider: walletAndMidnightProvider,
|
|
115
130
|
};
|
|
116
|
-
}
|
|
131
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ZswapSecretKeys, DustSecretKey } from "@
|
|
1
|
+
import type { ZswapSecretKeys, DustSecretKey } from "@midnightntwrk/ledger-v9";
|
|
2
|
+
import type { NetworkId } from "@midnightntwrk/wallet-sdk-abstractions";
|
|
2
3
|
import type { WalletFacade } from "@midnightntwrk/wallet-sdk-facade";
|
|
3
4
|
import type { UnshieldedKeystore } from "@midnightntwrk/wallet-sdk-unshielded-wallet";
|
|
4
5
|
|
|
@@ -61,10 +62,10 @@ export interface DeployConfig {
|
|
|
61
62
|
*/
|
|
62
63
|
export interface NetworkUrls {
|
|
63
64
|
/** Optional network ID override */
|
|
64
|
-
id?:
|
|
65
|
-
/** GraphQL indexer HTTP endpoint (default: http://127.0.0.1:8088/api/
|
|
65
|
+
id?: NetworkId.NetworkId;
|
|
66
|
+
/** GraphQL indexer HTTP endpoint (default: http://127.0.0.1:8088/api/v4/graphql)*/
|
|
66
67
|
indexer?: string;
|
|
67
|
-
/** GraphQL indexer WebSocket endpoint (default: ws://127.0.0.1:8088/api/
|
|
68
|
+
/** GraphQL indexer WebSocket endpoint (default: ws://127.0.0.1:8088/api/v4/graphql/ws)*/
|
|
68
69
|
indexerWS?: string;
|
|
69
70
|
/** Midnight node RPC endpoint (default: http://127.0.0.1:9944)*/
|
|
70
71
|
node?: string;
|
|
@@ -91,4 +92,4 @@ export interface WalletResult {
|
|
|
91
92
|
dustAddress: string;
|
|
92
93
|
unshieldedAddress: string;
|
|
93
94
|
unshieldedKeystore: UnshieldedKeystore;
|
|
94
|
-
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
import { nativeToken } from "@midnightntwrk/ledger-v9";
|
|
4
|
+
import type { NetworkId } from "@midnightntwrk/wallet-sdk-abstractions";
|
|
5
|
+
import type { WalletFacade } from "@midnightntwrk/wallet-sdk-facade";
|
|
6
|
+
import * as Rx from "rxjs";
|
|
7
|
+
|
|
8
|
+
import { deployMidnightContract } from "../src/deploy.ts";
|
|
9
|
+
import {
|
|
10
|
+
buildWalletFacade,
|
|
11
|
+
registerNightForDust,
|
|
12
|
+
resolveFacadeDustAvailableCoins,
|
|
13
|
+
syncAndWaitForFunds,
|
|
14
|
+
} from "../src/get-wallet-info.ts";
|
|
15
|
+
import type { NetworkUrls, WalletResult } from "../src/types.ts";
|
|
16
|
+
|
|
17
|
+
const networkId = "undeployed" as NetworkId.NetworkId;
|
|
18
|
+
const timeoutMs = Number(process.env.MIDNIGHT_WALLET_SYNC_TIMEOUT_MS ?? "600000");
|
|
19
|
+
const fundingAmount = BigInt(process.env.M3_FUNDING_AMOUNT ?? "10000000000000");
|
|
20
|
+
const sourceSeed = process.env.M3_SOURCE_SEED ??
|
|
21
|
+
"0000000000000000000000000000000000000000000000000000000000000001";
|
|
22
|
+
const recipientSeed = process.env.M3_RECIPIENT_SEED ??
|
|
23
|
+
"00000000000000000000000000000000000000000000000000000000000000a3";
|
|
24
|
+
const contractBaseDir = requiredEnv("M3_CONTRACT_BASE_DIR");
|
|
25
|
+
const contractModule = requiredEnv("M3_CONTRACT_MODULE");
|
|
26
|
+
|
|
27
|
+
const networkUrls: Required<NetworkUrls> = {
|
|
28
|
+
id: networkId,
|
|
29
|
+
indexer: requiredEnv("MIDNIGHT_INDEXER_HTTP"),
|
|
30
|
+
indexerWS: requiredEnv("MIDNIGHT_INDEXER_WS"),
|
|
31
|
+
node: requiredEnv("MIDNIGHT_NODE_HTTP"),
|
|
32
|
+
proofServer: requiredEnv("MIDNIGHT_PROOF_SERVER_URL"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const wallets: WalletResult[] = [];
|
|
36
|
+
let recipientWasPassedToDeploy = false;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
checkpoint("integration-start", {
|
|
40
|
+
runtime: `bun-${Bun.version}`,
|
|
41
|
+
networkId,
|
|
42
|
+
endpoints: networkUrls,
|
|
43
|
+
fundingAmount: fundingAmount.toString(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const source = await buildWalletFacade(networkUrls, sourceSeed, networkId);
|
|
47
|
+
wallets.push(source);
|
|
48
|
+
await syncAndWaitForFunds(source.wallet, { timeoutMs });
|
|
49
|
+
const sourceState = await waitForFacadeState(
|
|
50
|
+
source.wallet,
|
|
51
|
+
"source wallet with NIGHT and spendable DUST",
|
|
52
|
+
(state) =>
|
|
53
|
+
allStrictlyComplete(state) &&
|
|
54
|
+
unshieldedNight(state) >= fundingAmount &&
|
|
55
|
+
resolveFacadeDustAvailableCoins(state) >= 1,
|
|
56
|
+
);
|
|
57
|
+
checkpoint("source-wallet-ready", stateEvidence(sourceState));
|
|
58
|
+
|
|
59
|
+
const recipient = await buildWalletFacade(networkUrls, recipientSeed, networkId);
|
|
60
|
+
wallets.push(recipient);
|
|
61
|
+
await syncAndWaitForFunds(recipient.wallet, { timeoutMs });
|
|
62
|
+
const recipientBefore = await waitForFacadeState(
|
|
63
|
+
recipient.wallet,
|
|
64
|
+
"fresh recipient wallet fully synchronized",
|
|
65
|
+
allStrictlyComplete,
|
|
66
|
+
);
|
|
67
|
+
if (unshieldedNight(recipientBefore) !== 0n) {
|
|
68
|
+
throw new Error("recipient seed is not fresh: expected zero NIGHT before funding");
|
|
69
|
+
}
|
|
70
|
+
if (resolveFacadeDustAvailableCoins(recipientBefore) !== 0) {
|
|
71
|
+
throw new Error("recipient seed is not fresh: expected zero spendable DUST before registration");
|
|
72
|
+
}
|
|
73
|
+
checkpoint("recipient-wallet-strictly-complete", stateEvidence(recipientBefore));
|
|
74
|
+
|
|
75
|
+
const receiverAddress = await recipient.wallet.unshielded.getAddress();
|
|
76
|
+
const fundingRecipe = await source.wallet.transferTransaction(
|
|
77
|
+
[{
|
|
78
|
+
type: "unshielded",
|
|
79
|
+
outputs: [{
|
|
80
|
+
type: nativeToken().raw,
|
|
81
|
+
receiverAddress,
|
|
82
|
+
amount: fundingAmount,
|
|
83
|
+
}],
|
|
84
|
+
}],
|
|
85
|
+
{
|
|
86
|
+
shieldedSecretKeys: source.zswapSecretKeys,
|
|
87
|
+
dustSecretKey: source.dustSecretKey,
|
|
88
|
+
},
|
|
89
|
+
{ ttl: new Date(Date.now() + 30 * 60 * 1_000) },
|
|
90
|
+
);
|
|
91
|
+
if (fundingRecipe.type !== "UNPROVEN_TRANSACTION") {
|
|
92
|
+
throw new Error(`unexpected funding recipe type: ${fundingRecipe.type}`);
|
|
93
|
+
}
|
|
94
|
+
const signedFundingRecipe = await source.wallet.signRecipe(
|
|
95
|
+
fundingRecipe,
|
|
96
|
+
(payload) => source.unshieldedKeystore.signDataAsync(payload),
|
|
97
|
+
);
|
|
98
|
+
const fundingTx = await source.wallet.finalizeRecipe(signedFundingRecipe);
|
|
99
|
+
const fundingTxId = await source.wallet.submitTransaction(fundingTx);
|
|
100
|
+
checkpoint("fresh-wallet-funded", { fundingTxId: String(fundingTxId) });
|
|
101
|
+
|
|
102
|
+
const fundedState = await waitForFacadeState(
|
|
103
|
+
recipient.wallet,
|
|
104
|
+
"fresh recipient NIGHT UTXO",
|
|
105
|
+
(state) =>
|
|
106
|
+
allStrictlyComplete(state) &&
|
|
107
|
+
unshieldedNight(state) >= fundingAmount &&
|
|
108
|
+
state.unshielded.availableCoins.some(
|
|
109
|
+
(coin: { meta: { registeredForDustGeneration: boolean } }) =>
|
|
110
|
+
coin.meta.registeredForDustGeneration === false,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
checkpoint("recipient-funded-and-unregistered", stateEvidence(fundedState));
|
|
114
|
+
|
|
115
|
+
if (!await registerNightForDust(recipient)) {
|
|
116
|
+
throw new Error("fresh recipient NIGHT-to-DUST registration failed");
|
|
117
|
+
}
|
|
118
|
+
const registeredState = await waitForFacadeState(
|
|
119
|
+
recipient.wallet,
|
|
120
|
+
"registered wallet with spendable DUST",
|
|
121
|
+
(state) =>
|
|
122
|
+
allStrictlyComplete(state) && resolveFacadeDustAvailableCoins(state) >= 1,
|
|
123
|
+
);
|
|
124
|
+
checkpoint("recipient-dust-registered", stateEvidence(registeredState));
|
|
125
|
+
|
|
126
|
+
const fixture = await import(pathToFileURL(contractModule).href) as {
|
|
127
|
+
Counter: { Contract: unknown };
|
|
128
|
+
witnesses: unknown;
|
|
129
|
+
};
|
|
130
|
+
recipientWasPassedToDeploy = true;
|
|
131
|
+
const contractAddress = await deployMidnightContract(
|
|
132
|
+
{
|
|
133
|
+
contractName: "contract-counter",
|
|
134
|
+
contractFileName: "contract-counter.json",
|
|
135
|
+
contractClass: fixture.Counter.Contract,
|
|
136
|
+
witnesses: fixture.witnesses,
|
|
137
|
+
privateStateId: "counterPrivateState",
|
|
138
|
+
initialPrivateState: { privateCounter: 0 },
|
|
139
|
+
privateStateStoreName: "m3-counter-private-state",
|
|
140
|
+
baseDir: contractBaseDir,
|
|
141
|
+
},
|
|
142
|
+
networkUrls,
|
|
143
|
+
undefined,
|
|
144
|
+
{ walletResult: recipient },
|
|
145
|
+
);
|
|
146
|
+
if (!contractAddress) throw new Error("deployment returned an empty contract address");
|
|
147
|
+
checkpoint("dust-fee-deployment-finalized", {
|
|
148
|
+
contractAddress,
|
|
149
|
+
networkId,
|
|
150
|
+
dustCoinsBeforeDeployment: resolveFacadeDustAvailableCoins(registeredState),
|
|
151
|
+
});
|
|
152
|
+
checkpoint("integration-pass", { contractAddress, fundingTxId: String(fundingTxId) });
|
|
153
|
+
} finally {
|
|
154
|
+
await Promise.allSettled(
|
|
155
|
+
wallets.map((entry, index) =>
|
|
156
|
+
index === 1 && recipientWasPassedToDeploy
|
|
157
|
+
? Promise.resolve()
|
|
158
|
+
: entry.wallet.stop()
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function allStrictlyComplete(state: any): boolean {
|
|
164
|
+
return state.shielded.progress.isStrictlyComplete() &&
|
|
165
|
+
state.unshielded.progress.isStrictlyComplete() &&
|
|
166
|
+
state.dust.progress.isStrictlyComplete();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function unshieldedNight(state: any): bigint {
|
|
170
|
+
return state.unshielded.balances[nativeToken().raw] ?? 0n;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function stateEvidence(state: any): Record<string, unknown> {
|
|
174
|
+
return {
|
|
175
|
+
strictlyComplete: {
|
|
176
|
+
shielded: state.shielded.progress.isStrictlyComplete(),
|
|
177
|
+
unshielded: state.unshielded.progress.isStrictlyComplete(),
|
|
178
|
+
dust: state.dust.progress.isStrictlyComplete(),
|
|
179
|
+
},
|
|
180
|
+
unshieldedNight: unshieldedNight(state).toString(),
|
|
181
|
+
unregisteredNightCoins: state.unshielded.availableCoins.filter(
|
|
182
|
+
(coin: { meta: { registeredForDustGeneration: boolean } }) =>
|
|
183
|
+
coin.meta.registeredForDustGeneration === false,
|
|
184
|
+
).length,
|
|
185
|
+
dustAvailableCoins: resolveFacadeDustAvailableCoins(state),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function waitForFacadeState(
|
|
190
|
+
wallet: WalletFacade,
|
|
191
|
+
description: string,
|
|
192
|
+
predicate: (state: any) => boolean,
|
|
193
|
+
): Promise<any> {
|
|
194
|
+
let latest = "no state emitted";
|
|
195
|
+
return Rx.firstValueFrom(
|
|
196
|
+
wallet.state().pipe(
|
|
197
|
+
Rx.tap((state) => {
|
|
198
|
+
latest = JSON.stringify(stateEvidence(state));
|
|
199
|
+
}),
|
|
200
|
+
Rx.filter(predicate),
|
|
201
|
+
Rx.timeout({
|
|
202
|
+
first: timeoutMs,
|
|
203
|
+
with: () => Rx.throwError(
|
|
204
|
+
() => new Error(`timeout waiting for ${description}; latest=${latest}`),
|
|
205
|
+
),
|
|
206
|
+
}),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function checkpoint(name: string, details: Record<string, unknown> = {}): void {
|
|
212
|
+
console.log(JSON.stringify({
|
|
213
|
+
timestamp: new Date().toISOString(),
|
|
214
|
+
checkpoint: name,
|
|
215
|
+
...details,
|
|
216
|
+
}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function requiredEnv(name: string): string {
|
|
220
|
+
const value = process.env[name];
|
|
221
|
+
if (!value) throw new Error(`missing required environment variable ${name}`);
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import {
|
|
5
|
+
ZswapChainState,
|
|
6
|
+
addressFromKey,
|
|
7
|
+
sampleSigningKey,
|
|
8
|
+
signData,
|
|
9
|
+
signatureVerifyingKey,
|
|
10
|
+
verifySignature,
|
|
11
|
+
type SignatureKind,
|
|
12
|
+
} from "@midnightntwrk/ledger-v9";
|
|
13
|
+
import { WalletFacade } from "@midnightntwrk/wallet-sdk-facade";
|
|
14
|
+
import * as Rx from "rxjs";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
registerNightForDust,
|
|
18
|
+
resolveFacadeDustAvailableCoins,
|
|
19
|
+
resolveFacadeDustFundsReadiness,
|
|
20
|
+
waitForDustFunds,
|
|
21
|
+
} from "../src/get-wallet-info.ts";
|
|
22
|
+
import { defaultMidnightNetworkConfig } from "../src/midnight-env.ts";
|
|
23
|
+
import type { WalletResult } from "../src/types.ts";
|
|
24
|
+
|
|
25
|
+
describe("ledger-v9 compatibility traps", () => {
|
|
26
|
+
for (const kind of ["schnorr", "ecdsa"] satisfies SignatureKind[]) {
|
|
27
|
+
test(`${kind} signatures keep their tagged key and signature shapes`, () => {
|
|
28
|
+
const signingKey = sampleSigningKey(kind);
|
|
29
|
+
const verifyingKey = signatureVerifyingKey(signingKey);
|
|
30
|
+
const payload = new TextEncoder().encode(`effectstream-${kind}`);
|
|
31
|
+
const signature = signData(signingKey, payload);
|
|
32
|
+
|
|
33
|
+
expect(signingKey.tag).toBe(kind);
|
|
34
|
+
expect(verifyingKey.tag).toBe(kind);
|
|
35
|
+
expect(signature.tag).toBe(kind);
|
|
36
|
+
expect(signingKey.value).toMatch(/^[0-9a-f]+$/i);
|
|
37
|
+
expect(verifyingKey.value).toMatch(/^[0-9a-f]+$/i);
|
|
38
|
+
expect(signature.value).toMatch(/^[0-9a-f]+$/i);
|
|
39
|
+
expect(verifySignature(verifyingKey, payload, signature)).toBe(true);
|
|
40
|
+
expect(addressFromKey(verifyingKey)).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("postBlockUpdate accepts the required retentionDuration", () => {
|
|
45
|
+
const updated = new ZswapChainState().postBlockUpdate(
|
|
46
|
+
new Date("2026-08-23T00:00:00.000Z"),
|
|
47
|
+
3_600n,
|
|
48
|
+
);
|
|
49
|
+
expect(updated).toBeInstanceOf(ZswapChainState);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("wallet-v2 migration", () => {
|
|
54
|
+
test("wallet node client and Polkadot share one BN constructor", () => {
|
|
55
|
+
const nodeClientRoot = realpathSync(
|
|
56
|
+
`${import.meta.dir}/../node_modules/@midnightntwrk/wallet-sdk-node-client`,
|
|
57
|
+
);
|
|
58
|
+
const nodeClientRequire = createRequire(`${nodeClientRoot}/package.json`);
|
|
59
|
+
const polkadotUtilRequire = createRequire(
|
|
60
|
+
nodeClientRequire.resolve("@polkadot/util/package.json"),
|
|
61
|
+
);
|
|
62
|
+
const NodeClientBN = nodeClientRequire("bn.js");
|
|
63
|
+
const PolkadotBN = polkadotUtilRequire("bn.js");
|
|
64
|
+
const blockNumber = new PolkadotBN(7);
|
|
65
|
+
|
|
66
|
+
expect(nodeClientRequire("bn.js/package.json").version).toBe("5.2.3");
|
|
67
|
+
expect(polkadotUtilRequire("bn.js/package.json").version).toBe("5.2.3");
|
|
68
|
+
expect(PolkadotBN).toBe(NodeClientBN);
|
|
69
|
+
expect(blockNumber).toBeInstanceOf(NodeClientBN);
|
|
70
|
+
expect(blockNumber.toString()).toBe("7");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("WalletFacade exposes the async init entry point", () => {
|
|
74
|
+
expect(typeof WalletFacade.init).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("DUST readiness counts spendable coins, not accrued balance", () => {
|
|
78
|
+
expect(
|
|
79
|
+
resolveFacadeDustAvailableCoins({
|
|
80
|
+
dust: { availableCoins: [], balance: () => 99n },
|
|
81
|
+
}),
|
|
82
|
+
).toBe(0);
|
|
83
|
+
expect(
|
|
84
|
+
resolveFacadeDustAvailableCoins({
|
|
85
|
+
dust: { availableCoins: [{}], balance: () => 0n },
|
|
86
|
+
}),
|
|
87
|
+
).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("DUST readiness carries available coins when aggregate balance is zero", async () => {
|
|
91
|
+
const complete = { isStrictlyComplete: () => true };
|
|
92
|
+
const dustState = {
|
|
93
|
+
progress: complete,
|
|
94
|
+
availableCoins: [{ generatedNow: 10n }],
|
|
95
|
+
balance: () => 0n,
|
|
96
|
+
};
|
|
97
|
+
const wallet = { dust: { state: Rx.of(dustState) } } as unknown as WalletFacade;
|
|
98
|
+
|
|
99
|
+
const funds = await waitForDustFunds(wallet, {
|
|
100
|
+
timeoutMs: 100,
|
|
101
|
+
waitNonZero: true,
|
|
102
|
+
skipCatchUp: true,
|
|
103
|
+
dustPollIntervalMs: 0,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(funds).toEqual({
|
|
107
|
+
balance: 0n,
|
|
108
|
+
availableCoins: 1,
|
|
109
|
+
spendableCoins: 1,
|
|
110
|
+
ready: true,
|
|
111
|
+
});
|
|
112
|
+
expect(resolveFacadeDustFundsReadiness(dustState, 11n).ready).toBe(false);
|
|
113
|
+
expect(resolveFacadeDustFundsReadiness(dustState, 10n).ready).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("registration finalizes the returned UnprovenTransactionRecipe", async () => {
|
|
117
|
+
const calls: string[] = [];
|
|
118
|
+
const signingKey = sampleSigningKey("schnorr");
|
|
119
|
+
const verifyingKey = signatureVerifyingKey(signingKey);
|
|
120
|
+
const recipe = {
|
|
121
|
+
type: "UNPROVEN_TRANSACTION" as const,
|
|
122
|
+
transaction: {},
|
|
123
|
+
};
|
|
124
|
+
const complete = { isStrictlyComplete: () => true };
|
|
125
|
+
const dustState = {
|
|
126
|
+
progress: complete,
|
|
127
|
+
availableCoins: [{}],
|
|
128
|
+
balance: () => 1n,
|
|
129
|
+
};
|
|
130
|
+
const facadeState = {
|
|
131
|
+
dust: dustState,
|
|
132
|
+
unshielded: {
|
|
133
|
+
progress: complete,
|
|
134
|
+
availableCoins: [
|
|
135
|
+
{ meta: { registeredForDustGeneration: false } },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const wallet = {
|
|
140
|
+
state: () => Rx.of(facadeState),
|
|
141
|
+
dust: { state: Rx.of(dustState) },
|
|
142
|
+
estimateRegistration: async () => {
|
|
143
|
+
calls.push("estimate");
|
|
144
|
+
return { fee: 5n, dustGenerationEstimations: [] };
|
|
145
|
+
},
|
|
146
|
+
waitForGeneratedDust: async (
|
|
147
|
+
_coins: readonly unknown[],
|
|
148
|
+
requiredAmount: bigint,
|
|
149
|
+
) => {
|
|
150
|
+
calls.push("wait-generated");
|
|
151
|
+
expect(requiredAmount).toBe(5n);
|
|
152
|
+
},
|
|
153
|
+
registerNightUtxosForDustGeneration: async (
|
|
154
|
+
_coins: readonly unknown[],
|
|
155
|
+
key: unknown,
|
|
156
|
+
sign: (payload: Uint8Array) => Promise<unknown>,
|
|
157
|
+
) => {
|
|
158
|
+
calls.push("register");
|
|
159
|
+
expect(key).toEqual(verifyingKey);
|
|
160
|
+
await sign(new Uint8Array([1, 2, 3]));
|
|
161
|
+
return recipe;
|
|
162
|
+
},
|
|
163
|
+
finalizeRecipe: async (received: unknown) => {
|
|
164
|
+
calls.push("finalize");
|
|
165
|
+
expect(received).toBe(recipe);
|
|
166
|
+
return { finalized: true };
|
|
167
|
+
},
|
|
168
|
+
submitTransaction: async () => {
|
|
169
|
+
calls.push("submit");
|
|
170
|
+
return "tx-id";
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const walletResult = {
|
|
174
|
+
wallet,
|
|
175
|
+
unshieldedKeystore: {
|
|
176
|
+
getPublicKey: () => verifyingKey,
|
|
177
|
+
signDataAsync: async (payload: Uint8Array) =>
|
|
178
|
+
signData(signingKey, payload),
|
|
179
|
+
},
|
|
180
|
+
} as unknown as WalletResult;
|
|
181
|
+
|
|
182
|
+
expect(await registerNightForDust(walletResult)).toBe(true);
|
|
183
|
+
expect(calls).toEqual([
|
|
184
|
+
"estimate",
|
|
185
|
+
"wait-generated",
|
|
186
|
+
"register",
|
|
187
|
+
"finalize",
|
|
188
|
+
"submit",
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("network defaults", () => {
|
|
194
|
+
test("undeployed uses local GraphQL v4 endpoints", () => {
|
|
195
|
+
const config = defaultMidnightNetworkConfig("undeployed");
|
|
196
|
+
expect(config.indexer).toBe("http://127.0.0.1:8088/api/v4/graphql");
|
|
197
|
+
expect(config.indexerWS).toBe("ws://127.0.0.1:8088/api/v4/graphql/ws");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
for (const networkId of [
|
|
201
|
+
"mainnet",
|
|
202
|
+
"testnet",
|
|
203
|
+
"devnet",
|
|
204
|
+
"qanet",
|
|
205
|
+
"preview",
|
|
206
|
+
"preprod",
|
|
207
|
+
"stagenet",
|
|
208
|
+
"future-network",
|
|
209
|
+
]) {
|
|
210
|
+
test(`preserves deployed network ID ${networkId}`, () => {
|
|
211
|
+
const config = defaultMidnightNetworkConfig(networkId);
|
|
212
|
+
expect(config.networkId).toBe(networkId);
|
|
213
|
+
expect(config.indexer).toBe(
|
|
214
|
+
`https://indexer.${networkId}.midnight.network/api/v4/graphql`,
|
|
215
|
+
);
|
|
216
|
+
expect(config.node).toBe(`https://rpc.${networkId}.midnight.network`);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ZswapChainState,
|
|
3
|
+
type Signature,
|
|
4
|
+
type SignatureVerifyingKey,
|
|
5
|
+
type SigningKey,
|
|
6
|
+
} from "@midnightntwrk/ledger-v9";
|
|
7
|
+
|
|
8
|
+
const signingKey: SigningKey = { tag: "schnorr", value: "00" };
|
|
9
|
+
const verifyingKey: SignatureVerifyingKey = { tag: "ecdsa", value: "00" };
|
|
10
|
+
const signature: Signature = { tag: "schnorr", value: "00" };
|
|
11
|
+
void [signingKey, verifyingKey, signature];
|
|
12
|
+
|
|
13
|
+
// The legacy string representation must stay rejected by ledger-v9 types.
|
|
14
|
+
// @ts-expect-error ledger-v9 signing keys are tagged values
|
|
15
|
+
const legacySigningKey: SigningKey = "00";
|
|
16
|
+
// @ts-expect-error ledger-v9 verifying keys are tagged values
|
|
17
|
+
const legacyVerifyingKey: SignatureVerifyingKey = "00";
|
|
18
|
+
// @ts-expect-error ledger-v9 signatures are tagged values
|
|
19
|
+
const legacySignature: Signature = "00";
|
|
20
|
+
void [legacySigningKey, legacyVerifyingKey, legacySignature];
|
|
21
|
+
|
|
22
|
+
if (false) {
|
|
23
|
+
const state = new ZswapChainState();
|
|
24
|
+
state.postBlockUpdate(new Date(), 60n);
|
|
25
|
+
// @ts-expect-error ledger-v9 requires retentionDuration
|
|
26
|
+
state.postBlockUpdate(new Date());
|
|
27
|
+
}
|