@lawrenceliang-btc/atel-sdk 1.2.13 → 1.2.15
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 +8 -10
- package/bin/atel.mjs +188 -87
- package/bin/notification-action-helpers.mjs +76 -16
- package/dist/anchor/index.d.ts +2 -3
- package/dist/anchor/index.js +1 -2
- package/dist/endpoint/index.d.ts +0 -2
- package/dist/handshake/index.d.ts +0 -8
- package/package.json +1 -2
- package/skill/atel-agent/SKILL.md +1 -1
- package/skill/references/commercial.md +3 -14
- package/skill/references/executor.md +1 -1
- package/skill/references/onchain.md +7 -3
- package/skill/references/quickstart.md +1 -5
- package/skill/references/security.md +1 -1
- package/skill/references/workflows.md +1 -1
- package/dist/anchor/solana.d.ts +0 -95
- package/dist/anchor/solana.js +0 -298
package/dist/anchor/solana.js
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Solana Anchor Provider.
|
|
3
|
-
*
|
|
4
|
-
* Anchors hashes on Solana using the official Memo Program
|
|
5
|
-
* (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`).
|
|
6
|
-
*
|
|
7
|
-
* The memo content is formatted as `ATEL_ANCHOR:<hash>` so anchored
|
|
8
|
-
* transactions are easily identifiable.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* ⚠️ SECURITY: The `privateKey` (Base58-encoded) is used to sign
|
|
12
|
-
* transactions. Never hard-code it — use environment variables or a vault.
|
|
13
|
-
*/
|
|
14
|
-
import { Connection, Keypair, PublicKey, Transaction, TransactionInstruction, sendAndConfirmTransaction, } from '@solana/web3.js';
|
|
15
|
-
import bs58 from 'bs58';
|
|
16
|
-
/** Solana Memo Program address */
|
|
17
|
-
const MEMO_PROGRAM_ID = new PublicKey('MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr');
|
|
18
|
-
/** Prefix prepended to the hash in the memo (legacy format) */
|
|
19
|
-
const ANCHOR_PREFIX = 'ATEL_ANCHOR:';
|
|
20
|
-
/** V2 structured memo format: ATEL:1:<executorDID>:<requesterDID>:<taskId>:<trace_root> */
|
|
21
|
-
const ANCHOR_V2_PREFIX = 'ATEL:1:';
|
|
22
|
-
/**
|
|
23
|
-
* Anchor provider for the Solana blockchain.
|
|
24
|
-
*/
|
|
25
|
-
export class SolanaAnchorProvider {
|
|
26
|
-
name = 'Solana';
|
|
27
|
-
chain = 'solana';
|
|
28
|
-
/** Solana RPC connection */
|
|
29
|
-
connection;
|
|
30
|
-
/** Keypair for signing (undefined when no private key is supplied) */
|
|
31
|
-
keypair;
|
|
32
|
-
/** Default Solana mainnet-beta RPC URL */
|
|
33
|
-
static DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
|
|
34
|
-
/**
|
|
35
|
-
* @param config - RPC URL and optional private key.
|
|
36
|
-
* If `rpcUrl` is omitted, the Solana mainnet-beta default is used.
|
|
37
|
-
*/
|
|
38
|
-
constructor(config) {
|
|
39
|
-
this.connection = new Connection(config?.rpcUrl ?? SolanaAnchorProvider.DEFAULT_RPC_URL, 'confirmed');
|
|
40
|
-
if (config?.privateKey) {
|
|
41
|
-
// ⚠️ SECURITY: The keypair holds the private key in memory.
|
|
42
|
-
const secretKey = bs58.decode(config.privateKey);
|
|
43
|
-
this.keypair = Keypair.fromSecretKey(secretKey);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Encode a hash into the memo data buffer (v2 structured format).
|
|
48
|
-
* Falls back to legacy format if no metadata provided.
|
|
49
|
-
*/
|
|
50
|
-
static encodeMemo(hash, meta) {
|
|
51
|
-
if (meta?.executorDid && meta?.requesterDid && meta?.taskId) {
|
|
52
|
-
return Buffer.from(`${ANCHOR_V2_PREFIX}${meta.executorDid}:${meta.requesterDid}:${meta.taskId}:${hash}`, 'utf-8');
|
|
53
|
-
}
|
|
54
|
-
return Buffer.from(`${ANCHOR_PREFIX}${hash}`, 'utf-8');
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Decode a hash from memo data. Supports both v2 structured and legacy format.
|
|
58
|
-
*
|
|
59
|
-
* @returns The decoded hash, or `null` if the data doesn't match.
|
|
60
|
-
*/
|
|
61
|
-
static decodeMemo(data) {
|
|
62
|
-
const text = typeof data === 'string' ? data : Buffer.from(data).toString('utf-8');
|
|
63
|
-
// V2 structured format
|
|
64
|
-
if (text.startsWith(ANCHOR_V2_PREFIX)) {
|
|
65
|
-
const parts = text.slice(ANCHOR_V2_PREFIX.length).split(':');
|
|
66
|
-
if (parts.length >= 4)
|
|
67
|
-
return parts[parts.length - 1]; // last part is trace_root
|
|
68
|
-
}
|
|
69
|
-
// Legacy format
|
|
70
|
-
if (text.startsWith(ANCHOR_PREFIX)) {
|
|
71
|
-
return text.slice(ANCHOR_PREFIX.length);
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Decode full structured memo (v2 only).
|
|
77
|
-
* Returns null for legacy format memos.
|
|
78
|
-
*/
|
|
79
|
-
static decodeMemoV2(data) {
|
|
80
|
-
const text = typeof data === 'string' ? data : Buffer.from(data).toString('utf-8');
|
|
81
|
-
if (!text.startsWith(ANCHOR_V2_PREFIX))
|
|
82
|
-
return null;
|
|
83
|
-
const rest = text.slice(ANCHOR_V2_PREFIX.length);
|
|
84
|
-
// Format: executorDID:requesterDID:taskId:traceRoot
|
|
85
|
-
// DIDs contain colons (did:atel:ed25519:xxx), so we need smart parsing
|
|
86
|
-
// Split by ':' and reconstruct DIDs
|
|
87
|
-
const parts = rest.split(':');
|
|
88
|
-
// Minimum: did:atel:ed25519:key : did:atel:ed25519:key : taskId : traceRoot = 4+4+1+1 = 10 parts
|
|
89
|
-
if (parts.length < 10)
|
|
90
|
-
return null;
|
|
91
|
-
// Each DID is 4 parts: did:atel:ed25519:base58
|
|
92
|
-
const executorDid = parts.slice(0, 4).join(':');
|
|
93
|
-
const requesterDid = parts.slice(4, 8).join(':');
|
|
94
|
-
const taskId = parts[8];
|
|
95
|
-
const traceRoot = parts.slice(9).join(':');
|
|
96
|
-
if (!executorDid.startsWith('did:atel:') || !requesterDid.startsWith('did:atel:'))
|
|
97
|
-
return null;
|
|
98
|
-
return { version: 1, executorDid, requesterDid, taskId, traceRoot };
|
|
99
|
-
}
|
|
100
|
-
/** @inheritdoc */
|
|
101
|
-
async anchor(hash, metadata) {
|
|
102
|
-
if (!this.keypair) {
|
|
103
|
-
throw new Error('Solana: Cannot anchor without a private key');
|
|
104
|
-
}
|
|
105
|
-
const memoData = SolanaAnchorProvider.encodeMemo(hash, metadata);
|
|
106
|
-
const instruction = new TransactionInstruction({
|
|
107
|
-
keys: [{ pubkey: this.keypair.publicKey, isSigner: true, isWritable: true }],
|
|
108
|
-
programId: MEMO_PROGRAM_ID,
|
|
109
|
-
data: memoData,
|
|
110
|
-
});
|
|
111
|
-
const transaction = new Transaction().add(instruction);
|
|
112
|
-
try {
|
|
113
|
-
const signature = await sendAndConfirmTransaction(this.connection, transaction, [this.keypair], { commitment: 'confirmed' });
|
|
114
|
-
// Fetch the confirmed transaction to get slot/block info
|
|
115
|
-
let blockNumber;
|
|
116
|
-
try {
|
|
117
|
-
const txInfo = await this.connection.getTransaction(signature, {
|
|
118
|
-
commitment: 'confirmed',
|
|
119
|
-
maxSupportedTransactionVersion: 0,
|
|
120
|
-
});
|
|
121
|
-
blockNumber = txInfo?.slot;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
// Non-critical
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
hash,
|
|
128
|
-
txHash: signature,
|
|
129
|
-
chain: 'solana',
|
|
130
|
-
timestamp: Date.now(),
|
|
131
|
-
blockNumber,
|
|
132
|
-
metadata,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
-
throw new Error(`Solana anchor failed: ${message}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/** @inheritdoc */
|
|
141
|
-
async verify(hash, txHash) {
|
|
142
|
-
try {
|
|
143
|
-
const txInfo = await this.connection.getTransaction(txHash, {
|
|
144
|
-
commitment: 'confirmed',
|
|
145
|
-
maxSupportedTransactionVersion: 0,
|
|
146
|
-
});
|
|
147
|
-
if (!txInfo) {
|
|
148
|
-
return {
|
|
149
|
-
valid: false,
|
|
150
|
-
hash,
|
|
151
|
-
txHash,
|
|
152
|
-
chain: this.chain,
|
|
153
|
-
detail: 'Transaction not found',
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
// Search through instructions for a memo matching our hash
|
|
157
|
-
const message = txInfo.transaction.message;
|
|
158
|
-
let foundHash = null;
|
|
159
|
-
for (let i = 0; i < message.compiledInstructions.length; i++) {
|
|
160
|
-
const ix = message.compiledInstructions[i];
|
|
161
|
-
const keys = message.getAccountKeys();
|
|
162
|
-
const programId = keys.get(ix.programIdIndex);
|
|
163
|
-
if (programId?.equals(MEMO_PROGRAM_ID)) {
|
|
164
|
-
foundHash = SolanaAnchorProvider.decodeMemo(Buffer.from(ix.data));
|
|
165
|
-
if (foundHash)
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Fallback: check log messages for memo content
|
|
170
|
-
if (!foundHash && txInfo.meta?.logMessages) {
|
|
171
|
-
for (const log of txInfo.meta.logMessages) {
|
|
172
|
-
if (log.includes(ANCHOR_PREFIX)) {
|
|
173
|
-
const idx = log.indexOf(ANCHOR_PREFIX);
|
|
174
|
-
foundHash = log.slice(idx + ANCHOR_PREFIX.length);
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (foundHash === null) {
|
|
180
|
-
return {
|
|
181
|
-
valid: false,
|
|
182
|
-
hash,
|
|
183
|
-
txHash,
|
|
184
|
-
chain: this.chain,
|
|
185
|
-
detail: 'No anchor memo found in transaction',
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
const valid = foundHash === hash;
|
|
189
|
-
const blockTimestamp = txInfo.blockTime ? txInfo.blockTime * 1000 : undefined;
|
|
190
|
-
return {
|
|
191
|
-
valid,
|
|
192
|
-
hash,
|
|
193
|
-
txHash,
|
|
194
|
-
chain: this.chain,
|
|
195
|
-
blockTimestamp,
|
|
196
|
-
detail: valid
|
|
197
|
-
? 'Hash matches on-chain memo'
|
|
198
|
-
: `Hash mismatch: expected "${hash}", found "${foundHash}"`,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
203
|
-
return {
|
|
204
|
-
valid: false,
|
|
205
|
-
hash,
|
|
206
|
-
txHash,
|
|
207
|
-
chain: this.chain,
|
|
208
|
-
detail: `Verification error: ${message}`,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
/** @inheritdoc */
|
|
213
|
-
async lookup(hash) {
|
|
214
|
-
// On-chain lookup without an indexer is not feasible for Solana.
|
|
215
|
-
// In production, integrate with a Solana indexer or Helius API.
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
/** @inheritdoc */
|
|
219
|
-
async isAvailable() {
|
|
220
|
-
try {
|
|
221
|
-
await this.connection.getSlot();
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Query all ATEL anchor transactions for a given wallet address.
|
|
230
|
-
* Parses v2 structured memos to extract DID and task info.
|
|
231
|
-
*
|
|
232
|
-
* @param walletAddress - Solana wallet public key (base58)
|
|
233
|
-
* @param options - limit (default 100), filterDid (only return records involving this DID)
|
|
234
|
-
* @returns Array of parsed anchor memos with tx info
|
|
235
|
-
*/
|
|
236
|
-
async queryByWallet(walletAddress, options) {
|
|
237
|
-
const limit = options?.limit ?? 100;
|
|
238
|
-
const pubkey = new PublicKey(walletAddress);
|
|
239
|
-
const results = [];
|
|
240
|
-
try {
|
|
241
|
-
const signatures = await this.connection.getSignaturesForAddress(pubkey, { limit });
|
|
242
|
-
for (const sig of signatures) {
|
|
243
|
-
try {
|
|
244
|
-
const txInfo = await this.connection.getTransaction(sig.signature, {
|
|
245
|
-
commitment: 'confirmed',
|
|
246
|
-
maxSupportedTransactionVersion: 0,
|
|
247
|
-
});
|
|
248
|
-
if (!txInfo)
|
|
249
|
-
continue;
|
|
250
|
-
// Search for ATEL memo in instructions
|
|
251
|
-
const message = txInfo.transaction.message;
|
|
252
|
-
for (let i = 0; i < message.compiledInstructions.length; i++) {
|
|
253
|
-
const ix = message.compiledInstructions[i];
|
|
254
|
-
const keys = message.getAccountKeys();
|
|
255
|
-
const programId = keys.get(ix.programIdIndex);
|
|
256
|
-
if (!programId?.equals(MEMO_PROGRAM_ID))
|
|
257
|
-
continue;
|
|
258
|
-
const memoText = Buffer.from(ix.data).toString('utf-8');
|
|
259
|
-
const parsed = SolanaAnchorProvider.decodeMemoV2(memoText);
|
|
260
|
-
if (!parsed)
|
|
261
|
-
continue;
|
|
262
|
-
// Filter by DID if requested
|
|
263
|
-
if (options?.filterDid && parsed.executorDid !== options.filterDid && parsed.requesterDid !== options.filterDid)
|
|
264
|
-
continue;
|
|
265
|
-
results.push({
|
|
266
|
-
...parsed,
|
|
267
|
-
txHash: sig.signature,
|
|
268
|
-
blockTime: txInfo.blockTime ? txInfo.blockTime * 1000 : undefined,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
// Fallback: check log messages
|
|
272
|
-
if (txInfo.meta?.logMessages) {
|
|
273
|
-
for (const log of txInfo.meta.logMessages) {
|
|
274
|
-
if (!log.includes(ANCHOR_V2_PREFIX))
|
|
275
|
-
continue;
|
|
276
|
-
const idx = log.indexOf(ANCHOR_V2_PREFIX);
|
|
277
|
-
const parsed = SolanaAnchorProvider.decodeMemoV2(log.slice(idx));
|
|
278
|
-
if (!parsed)
|
|
279
|
-
continue;
|
|
280
|
-
if (options?.filterDid && parsed.executorDid !== options.filterDid && parsed.requesterDid !== options.filterDid)
|
|
281
|
-
continue;
|
|
282
|
-
if (!results.some(r => r.txHash === sig.signature)) {
|
|
283
|
-
results.push({ ...parsed, txHash: sig.signature, blockTime: txInfo.blockTime ? txInfo.blockTime * 1000 : undefined });
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
// Skip individual tx failures
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// Query failed — return empty
|
|
295
|
-
}
|
|
296
|
-
return results;
|
|
297
|
-
}
|
|
298
|
-
}
|