@layerzerolabs/test-utils-stellar 0.2.122
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/LICENSE +23 -0
- package/README.md +108 -0
- package/dist/4UKUSHEU.js +30 -0
- package/dist/4UKUSHEU.js.map +1 -0
- package/dist/7DZ4SVEO.js +46 -0
- package/dist/7DZ4SVEO.js.map +1 -0
- package/dist/CYINPPER.js +293 -0
- package/dist/CYINPPER.js.map +1 -0
- package/dist/DNRE4ENZ.js +56 -0
- package/dist/DNRE4ENZ.js.map +1 -0
- package/dist/GAGBEOCZ.js +25 -0
- package/dist/GAGBEOCZ.js.map +1 -0
- package/dist/PMDPSIA2.js +149 -0
- package/dist/PMDPSIA2.js.map +1 -0
- package/dist/TYAJMKGY.js +183 -0
- package/dist/TYAJMKGY.js.map +1 -0
- package/dist/VRABOZPX.js +364 -0
- package/dist/VRABOZPX.js.map +1 -0
- package/dist/VUOMXK5T.js +6 -0
- package/dist/VUOMXK5T.js.map +1 -0
- package/dist/ZWKXKZVT.js +394 -0
- package/dist/ZWKXKZVT.js.map +1 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -0
- package/dist/deploy.d.ts +43 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +4 -0
- package/dist/deploy.js.map +1 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +5 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/localnet-global-setup.d.ts +7 -0
- package/dist/localnet-global-setup.d.ts.map +1 -0
- package/dist/localnet-global-setup.js +6 -0
- package/dist/localnet-global-setup.js.map +1 -0
- package/dist/localnet.d.ts +37 -0
- package/dist/localnet.d.ts.map +1 -0
- package/dist/localnet.js +5 -0
- package/dist/localnet.js.map +1 -0
- package/dist/protocol-global-setup.d.ts +58 -0
- package/dist/protocol-global-setup.d.ts.map +1 -0
- package/dist/protocol-global-setup.js +7 -0
- package/dist/protocol-global-setup.js.map +1 -0
- package/dist/scan.d.ts +59 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +4 -0
- package/dist/scan.js.map +1 -0
- package/dist/secp256k1.d.ts +23 -0
- package/dist/secp256k1.d.ts.map +1 -0
- package/dist/secp256k1.js +4 -0
- package/dist/secp256k1.js.map +1 -0
- package/dist/utils.d.ts +54 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +5 -0
- package/dist/utils.js.map +1 -0
- package/package.json +50 -0
- package/src/client.ts +37 -0
- package/src/deploy.ts +316 -0
- package/src/env.ts +104 -0
- package/src/index.ts +40 -0
- package/src/localnet-global-setup.ts +30 -0
- package/src/localnet.ts +406 -0
- package/src/protocol-global-setup.ts +747 -0
- package/src/scan.ts +255 -0
- package/src/secp256k1.ts +59 -0
- package/src/utils.ts +642 -0
package/src/scan.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { rpc, xdr } from '@stellar/stellar-sdk';
|
|
2
|
+
|
|
3
|
+
import type { StellarTestEnv } from './env.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Event filter for scanning contract events
|
|
7
|
+
*/
|
|
8
|
+
export interface EventFilter {
|
|
9
|
+
contractId: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parsed contract event
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedContractEvent {
|
|
16
|
+
type: string;
|
|
17
|
+
contractId: string;
|
|
18
|
+
topics: any[];
|
|
19
|
+
data: any;
|
|
20
|
+
ledger: number;
|
|
21
|
+
txHash: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* PacketSent event structure from endpoint_v2.rs
|
|
26
|
+
*/
|
|
27
|
+
export interface PacketSentEvent {
|
|
28
|
+
encoded_packet: Buffer;
|
|
29
|
+
options: Buffer;
|
|
30
|
+
send_library: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_LEDGER_WAIT_ATTEMPTS = 10;
|
|
34
|
+
export const DEFAULT_LEDGER_WAIT_DELAY_MS = 1000;
|
|
35
|
+
|
|
36
|
+
export type LatestLedgerReader = () => Promise<{ sequence: number }>;
|
|
37
|
+
|
|
38
|
+
export interface ResolveScanEndLedgerOptions {
|
|
39
|
+
maxAttempts?: number;
|
|
40
|
+
delayMs?: number;
|
|
41
|
+
sleep?: (ms: number) => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* RPC `getEvents` rejects ranges where start is past the tip, and treats end as
|
|
46
|
+
* exclusive in practice. Require tip strictly greater than the scan start ledger.
|
|
47
|
+
*/
|
|
48
|
+
export function assertValidScanLedgerRange(startLedger: number, endLedger: number): void {
|
|
49
|
+
if (endLedger <= startLedger) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Cannot scan events: localnet tip ${endLedger} did not advance past start ledger ${startLedger}. ` +
|
|
52
|
+
`Calling getEvents with that range would fail (invalid / empty ledger window). ` +
|
|
53
|
+
`The network is likely stalled or the transaction ledger was not yet closed.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ensure latest ledger is strictly after `startLedgerSequence` before scanning.
|
|
60
|
+
* Returns the tip to use as `endLedger`.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveScanEndLedger(
|
|
63
|
+
getLatestLedger: LatestLedgerReader,
|
|
64
|
+
startLedgerSequence: number,
|
|
65
|
+
options: ResolveScanEndLedgerOptions = {},
|
|
66
|
+
): Promise<number> {
|
|
67
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_LEDGER_WAIT_ATTEMPTS;
|
|
68
|
+
const delayMs = options.delayMs ?? DEFAULT_LEDGER_WAIT_DELAY_MS;
|
|
69
|
+
const sleep =
|
|
70
|
+
options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
71
|
+
|
|
72
|
+
let endLedger = (await getLatestLedger()).sequence;
|
|
73
|
+
if (endLedger > startLedgerSequence) {
|
|
74
|
+
return endLedger;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(
|
|
78
|
+
` Waiting for tip to advance past ledger ${startLedgerSequence} (current tip ${endLedger})...`,
|
|
79
|
+
);
|
|
80
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
81
|
+
await sleep(delayMs);
|
|
82
|
+
endLedger = (await getLatestLedger()).sequence;
|
|
83
|
+
if (endLedger > startLedgerSequence) {
|
|
84
|
+
return endLedger;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fail closed: do not call getEvents with start >= tip (invalid / empty window).
|
|
89
|
+
assertValidScanLedgerRange(startLedgerSequence, endLedger);
|
|
90
|
+
return endLedger;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Scan for contract events within a ledger range
|
|
95
|
+
*/
|
|
96
|
+
export async function scanEvents(
|
|
97
|
+
env: StellarTestEnv,
|
|
98
|
+
startLedgerSequence: number,
|
|
99
|
+
filter: EventFilter,
|
|
100
|
+
): Promise<ParsedContractEvent[]> {
|
|
101
|
+
const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
|
|
102
|
+
|
|
103
|
+
console.log('🔍 Scanning events from ledger', startLedgerSequence);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const endLedger = await resolveScanEndLedger(
|
|
107
|
+
() => server.getLatestLedger(),
|
|
108
|
+
startLedgerSequence,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const events = await server.getEvents({
|
|
112
|
+
startLedger: startLedgerSequence,
|
|
113
|
+
filters: [{ type: 'contract', contractIds: [filter.contractId] }],
|
|
114
|
+
endLedger: endLedger,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const parsedEvents: ParsedContractEvent[] = [];
|
|
118
|
+
|
|
119
|
+
for (const event of events.events || []) {
|
|
120
|
+
try {
|
|
121
|
+
const parsedEvent: ParsedContractEvent = {
|
|
122
|
+
type: event.type,
|
|
123
|
+
contractId: event.contractId?.contractId() || '',
|
|
124
|
+
topics: event.topic,
|
|
125
|
+
data: event.value ? xdr.ScVal.fromXDR(event.value.toXDR()) : undefined,
|
|
126
|
+
ledger: event.ledger,
|
|
127
|
+
txHash: event.txHash,
|
|
128
|
+
};
|
|
129
|
+
parsedEvents.push(parsedEvent);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.warn('Failed to parse event:', e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.log(`✅ Found ${parsedEvents.length} events`);
|
|
136
|
+
return parsedEvents;
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
console.error('Error scanning events:', error.message);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Scan for PacketSent events from the Endpoint contract
|
|
145
|
+
*/
|
|
146
|
+
export async function scanPacketSentEvents(
|
|
147
|
+
env: StellarTestEnv,
|
|
148
|
+
endpointAddress: string,
|
|
149
|
+
startLedger: number,
|
|
150
|
+
): Promise<PacketSentEvent[]> {
|
|
151
|
+
console.log('📦 Scanning for PacketSent events...');
|
|
152
|
+
const events = await scanEvents(env, startLedger, {
|
|
153
|
+
contractId: endpointAddress,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Filter for PacketSent events
|
|
157
|
+
// PacketSent event has topics like ["PacketSent"] or similar
|
|
158
|
+
const packetSentEvents: PacketSentEvent[] = [];
|
|
159
|
+
|
|
160
|
+
for (const event of events) {
|
|
161
|
+
try {
|
|
162
|
+
// Check if this is a PacketSent event
|
|
163
|
+
// The first topic should be the event name
|
|
164
|
+
const firstTopic = event.topics[0];
|
|
165
|
+
|
|
166
|
+
// Convert ScVal to string to check event name
|
|
167
|
+
let eventName = '';
|
|
168
|
+
if (firstTopic && firstTopic._switch?.name === 'scvSymbol') {
|
|
169
|
+
eventName = firstTopic.value()?.toString() || '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (eventName === 'packet_sent') {
|
|
173
|
+
// Parse the event data
|
|
174
|
+
// PacketSent { encoded_packet, options, send_library }
|
|
175
|
+
const eventData = event.data;
|
|
176
|
+
|
|
177
|
+
// Try to extract fields from the struct
|
|
178
|
+
if (eventData && eventData._switch?.name === 'scvMap') {
|
|
179
|
+
const map = eventData.value();
|
|
180
|
+
const packetEvent: Partial<PacketSentEvent> = {};
|
|
181
|
+
|
|
182
|
+
for (const entry of map || []) {
|
|
183
|
+
const key = entry.key().value()?.toString();
|
|
184
|
+
const value = entry.val();
|
|
185
|
+
|
|
186
|
+
if (key === 'encoded_packet' && value._switch?.name === 'scvBytes') {
|
|
187
|
+
packetEvent.encoded_packet = Buffer.from(value.value());
|
|
188
|
+
} else if (key === 'options' && value._switch?.name === 'scvBytes') {
|
|
189
|
+
packetEvent.options = Buffer.from(value.value());
|
|
190
|
+
} else if (key === 'send_library' && value._switch?.name === 'scvAddress') {
|
|
191
|
+
packetEvent.send_library = value.value()?.toString() || '';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (
|
|
196
|
+
packetEvent.encoded_packet &&
|
|
197
|
+
packetEvent.options !== undefined &&
|
|
198
|
+
packetEvent.send_library
|
|
199
|
+
) {
|
|
200
|
+
packetSentEvents.push(packetEvent as PacketSentEvent);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.warn('Failed to parse PacketSent event:', e);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log(`✅ Found ${packetSentEvents.length} PacketSent events`);
|
|
210
|
+
return packetSentEvents;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Wait for new ledgers and scan for events
|
|
215
|
+
*/
|
|
216
|
+
export async function waitAndScanEvents(
|
|
217
|
+
env: StellarTestEnv,
|
|
218
|
+
startLedger: number,
|
|
219
|
+
contractAddress: string,
|
|
220
|
+
eventName: string,
|
|
221
|
+
maxWaitSeconds: number = 30,
|
|
222
|
+
): Promise<ParsedContractEvent[]> {
|
|
223
|
+
const server = new rpc.Server(env.RPC_URL, { allowHttp: true });
|
|
224
|
+
const endTime = Date.now() + maxWaitSeconds * 1000;
|
|
225
|
+
|
|
226
|
+
console.log(`⏳ Waiting for ${eventName} events...`);
|
|
227
|
+
|
|
228
|
+
while (Date.now() < endTime) {
|
|
229
|
+
const currentLedger = await server.getLatestLedger();
|
|
230
|
+
|
|
231
|
+
if (currentLedger.sequence > startLedger) {
|
|
232
|
+
const events = await scanEvents(env, startLedger, {
|
|
233
|
+
contractId: contractAddress,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const matchingEvents = events.filter((e) => {
|
|
237
|
+
const firstTopic = e.topics[0];
|
|
238
|
+
if (firstTopic && firstTopic._switch?.name === 'scvSymbol') {
|
|
239
|
+
const name = firstTopic.value()?.toString() || '';
|
|
240
|
+
return name === eventName;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (matchingEvents.length > 0) {
|
|
246
|
+
return matchingEvents;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Wait 1 second before checking again
|
|
251
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
throw new Error(`Timeout waiting for ${eventName} events`);
|
|
255
|
+
}
|
package/src/secp256k1.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { keccak_256 } from '@noble/hashes/sha3';
|
|
2
|
+
import * as secp from '@noble/secp256k1';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A secp256k1 key pair with private key and derived Ethereum-style address.
|
|
6
|
+
* Used for DVN multisig signing.
|
|
7
|
+
*/
|
|
8
|
+
export class Secp256k1KeyPair {
|
|
9
|
+
private privateKey: Uint8Array;
|
|
10
|
+
public readonly ethAddress: Buffer;
|
|
11
|
+
|
|
12
|
+
constructor(privateKey: Uint8Array | Buffer | string) {
|
|
13
|
+
if (typeof privateKey === 'string') {
|
|
14
|
+
// Remove 0x prefix if present
|
|
15
|
+
const hex = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey;
|
|
16
|
+
this.privateKey = Buffer.from(hex, 'hex');
|
|
17
|
+
} else {
|
|
18
|
+
this.privateKey = new Uint8Array(privateKey);
|
|
19
|
+
}
|
|
20
|
+
this.ethAddress = this.deriveEthAddress();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a random key pair.
|
|
25
|
+
*/
|
|
26
|
+
static generate(): Secp256k1KeyPair {
|
|
27
|
+
const privateKey = secp.utils.randomPrivateKey();
|
|
28
|
+
return new Secp256k1KeyPair(privateKey);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Derive Ethereum-style address from the public key.
|
|
33
|
+
* Address = last 20 bytes of keccak256(uncompressed_pubkey[1:65])
|
|
34
|
+
*/
|
|
35
|
+
private deriveEthAddress(): Buffer {
|
|
36
|
+
const publicKey = secp.getPublicKey(this.privateKey, false); // uncompressed
|
|
37
|
+
const pubkeyWithoutPrefix = publicKey.slice(1); // remove 0x04 prefix
|
|
38
|
+
const hash = keccak_256(pubkeyWithoutPrefix);
|
|
39
|
+
return Buffer.from(hash.slice(12)); // last 20 bytes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sign a 32-byte digest and return a 65-byte signature (r || s || v).
|
|
44
|
+
*/
|
|
45
|
+
async sign(digest: Uint8Array): Promise<Buffer> {
|
|
46
|
+
const [signature, recoveryId] = await secp.sign(digest, this.privateKey, {
|
|
47
|
+
canonical: true,
|
|
48
|
+
recovered: true,
|
|
49
|
+
der: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const v = 27 + recoveryId;
|
|
53
|
+
const result = Buffer.alloc(65);
|
|
54
|
+
result.set(signature, 0); // r (32 bytes) + s (32 bytes)
|
|
55
|
+
result[64] = v;
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
}
|