@1sat/cli 0.0.1
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/bin/1sat +0 -0
- package/package.json +32 -0
- package/src/args.ts +100 -0
- package/src/cli.ts +105 -0
- package/src/commands/action.ts +79 -0
- package/src/commands/config.ts +109 -0
- package/src/commands/identity.ts +133 -0
- package/src/commands/init.ts +172 -0
- package/src/commands/locks.ts +126 -0
- package/src/commands/opns.ts +73 -0
- package/src/commands/ordinals.ts +355 -0
- package/src/commands/social.ts +56 -0
- package/src/commands/sweep.ts +54 -0
- package/src/commands/tokens.ts +194 -0
- package/src/commands/tx.ts +65 -0
- package/src/commands/wallet.ts +218 -0
- package/src/config.ts +97 -0
- package/src/context.ts +63 -0
- package/src/help.ts +117 -0
- package/src/keys.ts +88 -0
- package/src/output.ts +82 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordinals commands - list, mint, transfer, sell, cancel, buy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
cancelListing,
|
|
7
|
+
deriveDepositAddresses,
|
|
8
|
+
getOrdinals,
|
|
9
|
+
inscribe,
|
|
10
|
+
listOrdinal,
|
|
11
|
+
purchaseOrdinal,
|
|
12
|
+
transferOrdinals,
|
|
13
|
+
} from '@1sat/actions'
|
|
14
|
+
import { Utils } from '@bsv/sdk'
|
|
15
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
16
|
+
import { readFileSync } from 'node:fs'
|
|
17
|
+
import { basename, extname } from 'node:path'
|
|
18
|
+
import type { GlobalFlags } from '../args'
|
|
19
|
+
import { extractFlag } from '../args'
|
|
20
|
+
import { loadContext } from '../context'
|
|
21
|
+
import { printCommandHelp } from '../help'
|
|
22
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
23
|
+
import { fatal, formatLabel, formatValue, output } from '../output'
|
|
24
|
+
|
|
25
|
+
export async function handleOrdinalsCommand(
|
|
26
|
+
args: string[],
|
|
27
|
+
opts: GlobalFlags,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const [subcommand, ...rest] = args
|
|
30
|
+
|
|
31
|
+
switch (subcommand) {
|
|
32
|
+
case 'list':
|
|
33
|
+
return ordinalsList(rest, opts)
|
|
34
|
+
case 'mint':
|
|
35
|
+
return ordinalsMint(rest, opts)
|
|
36
|
+
case 'transfer':
|
|
37
|
+
return ordinalsTransfer(rest, opts)
|
|
38
|
+
case 'sell':
|
|
39
|
+
return ordinalsSell(rest, opts)
|
|
40
|
+
case 'cancel':
|
|
41
|
+
return ordinalsCancel(rest, opts)
|
|
42
|
+
case 'buy':
|
|
43
|
+
return ordinalsBuy(rest, opts)
|
|
44
|
+
default:
|
|
45
|
+
printCommandHelp('ordinals', {
|
|
46
|
+
list: 'List owned ordinals/inscriptions',
|
|
47
|
+
mint: 'Mint a new ordinal inscription (--file <path> --type <mime>)',
|
|
48
|
+
transfer: 'Transfer an ordinal (--outpoint <op> --to <addr>)',
|
|
49
|
+
sell: 'List an ordinal for sale (--outpoint <op> --price <sats>)',
|
|
50
|
+
cancel: 'Cancel an ordinal listing (--outpoint <op>)',
|
|
51
|
+
buy: 'Purchase a listed ordinal (--outpoint <op>)',
|
|
52
|
+
})
|
|
53
|
+
if (subcommand && subcommand !== 'help') {
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function ordinalsList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
60
|
+
const privateKey = await loadKey(resolvePassword())
|
|
61
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
62
|
+
chain: opts.chain,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await getOrdinals.execute(ctx, {
|
|
67
|
+
limit: 100,
|
|
68
|
+
offset: 0,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
output(result.outputs, opts)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (result.outputs.length === 0) {
|
|
77
|
+
output('No ordinals found.', opts)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const o of result.outputs) {
|
|
82
|
+
const typeTag =
|
|
83
|
+
o.tags?.find((t) => t.startsWith('type:'))?.slice(5) ?? 'unknown'
|
|
84
|
+
const originTag =
|
|
85
|
+
o.tags?.find((t) => t.startsWith('origin:'))?.slice(7) ?? ''
|
|
86
|
+
const nameTag = o.tags?.find((t) => t.startsWith('name:'))?.slice(5) ?? ''
|
|
87
|
+
|
|
88
|
+
console.log(
|
|
89
|
+
` ${formatValue(o.outpoint)} ${formatLabel(typeTag)}${nameTag ? ` ${nameTag}` : ''}${originTag ? ` origin:${originTag}` : ''}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`\n ${result.outputs.length} ordinal(s) found.`)
|
|
94
|
+
} finally {
|
|
95
|
+
await destroy()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const MIME_TYPES: Record<string, string> = {
|
|
100
|
+
'.txt': 'text/plain',
|
|
101
|
+
'.html': 'text/html',
|
|
102
|
+
'.css': 'text/css',
|
|
103
|
+
'.js': 'application/javascript',
|
|
104
|
+
'.json': 'application/json',
|
|
105
|
+
'.xml': 'application/xml',
|
|
106
|
+
'.svg': 'image/svg+xml',
|
|
107
|
+
'.png': 'image/png',
|
|
108
|
+
'.jpg': 'image/jpeg',
|
|
109
|
+
'.jpeg': 'image/jpeg',
|
|
110
|
+
'.gif': 'image/gif',
|
|
111
|
+
'.webp': 'image/webp',
|
|
112
|
+
'.bmp': 'image/bmp',
|
|
113
|
+
'.mp3': 'audio/mpeg',
|
|
114
|
+
'.wav': 'audio/wav',
|
|
115
|
+
'.mp4': 'video/mp4',
|
|
116
|
+
'.webm': 'video/webm',
|
|
117
|
+
'.pdf': 'application/pdf',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function ordinalsMint(args: string[], opts: GlobalFlags): Promise<void> {
|
|
121
|
+
const file = extractFlag(args, '--file')
|
|
122
|
+
const type = extractFlag(args, '--type')
|
|
123
|
+
|
|
124
|
+
if (!file) fatal('Missing --file <path>')
|
|
125
|
+
|
|
126
|
+
const contentType = type ?? MIME_TYPES[extname(file).toLowerCase()]
|
|
127
|
+
if (!contentType) {
|
|
128
|
+
fatal(
|
|
129
|
+
`Cannot detect content type for ${basename(file)}. Use --type <mime-type>`,
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let fileBytes: Uint8Array
|
|
134
|
+
try {
|
|
135
|
+
fileBytes = readFileSync(file)
|
|
136
|
+
} catch (err) {
|
|
137
|
+
fatal(`Failed to read file: ${(err as Error).message}`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const base64Content = Utils.toBase64(Array.from(fileBytes))
|
|
141
|
+
|
|
142
|
+
if (!opts.yes) {
|
|
143
|
+
const ok = await confirm({
|
|
144
|
+
message: `Inscribe ${basename(file)} (${contentType}, ${fileBytes.length} bytes)?`,
|
|
145
|
+
})
|
|
146
|
+
if (isCancel(ok) || !ok) {
|
|
147
|
+
fatal('Inscription cancelled.')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const privateKey = await loadKey(resolvePassword())
|
|
152
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
153
|
+
chain: opts.chain,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await inscribe.execute(ctx, {
|
|
158
|
+
base64Content,
|
|
159
|
+
contentType,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (result.error) {
|
|
163
|
+
fatal(result.error)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
167
|
+
} finally {
|
|
168
|
+
await destroy()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ordinalsTransfer(
|
|
173
|
+
args: string[],
|
|
174
|
+
opts: GlobalFlags,
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
const outpoint = extractFlag(args, '--outpoint')
|
|
177
|
+
const to = extractFlag(args, '--to')
|
|
178
|
+
|
|
179
|
+
if (!outpoint) fatal('Missing --outpoint <txid.vout>')
|
|
180
|
+
if (!to) fatal('Missing --to <address>')
|
|
181
|
+
|
|
182
|
+
if (!opts.yes) {
|
|
183
|
+
const ok = await confirm({
|
|
184
|
+
message: `Transfer ordinal ${outpoint} to ${to}?`,
|
|
185
|
+
})
|
|
186
|
+
if (isCancel(ok) || !ok) {
|
|
187
|
+
fatal('Transfer cancelled.')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const privateKey = await loadKey(resolvePassword())
|
|
192
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
193
|
+
chain: opts.chain,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Look up the ordinal from the wallet
|
|
198
|
+
const ordinalsResult = await getOrdinals.execute(ctx, { limit: 10000 })
|
|
199
|
+
const ordinal = ordinalsResult.outputs.find((o) => o.outpoint === outpoint)
|
|
200
|
+
if (!ordinal) {
|
|
201
|
+
fatal(`Ordinal not found in wallet: ${outpoint}`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = await transferOrdinals.execute(ctx, {
|
|
205
|
+
transfers: [{ ordinal, address: to }],
|
|
206
|
+
inputBEEF: ordinalsResult.BEEF as number[] | undefined,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if (result.error) {
|
|
210
|
+
fatal(result.error)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
214
|
+
} finally {
|
|
215
|
+
await destroy()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function ordinalsSell(args: string[], opts: GlobalFlags): Promise<void> {
|
|
220
|
+
const outpoint = extractFlag(args, '--outpoint')
|
|
221
|
+
const priceStr = extractFlag(args, '--price')
|
|
222
|
+
|
|
223
|
+
if (!outpoint) fatal('Missing --outpoint <txid.vout>')
|
|
224
|
+
if (!priceStr) fatal('Missing --price <satoshis>')
|
|
225
|
+
|
|
226
|
+
const price = Number(priceStr)
|
|
227
|
+
if (!Number.isFinite(price) || price <= 0) {
|
|
228
|
+
fatal('--price must be a positive number')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!opts.yes) {
|
|
232
|
+
const ok = await confirm({
|
|
233
|
+
message: `List ordinal ${outpoint} for sale at ${price} satoshis?`,
|
|
234
|
+
})
|
|
235
|
+
if (isCancel(ok) || !ok) {
|
|
236
|
+
fatal('Listing cancelled.')
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const privateKey = await loadKey(resolvePassword())
|
|
241
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
242
|
+
chain: opts.chain,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// Look up the ordinal from the wallet
|
|
247
|
+
const ordinalsResult = await getOrdinals.execute(ctx, { limit: 10000 })
|
|
248
|
+
const ordinal = ordinalsResult.outputs.find((o) => o.outpoint === outpoint)
|
|
249
|
+
if (!ordinal) {
|
|
250
|
+
fatal(`Ordinal not found in wallet: ${outpoint}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Derive a BRC-29 address to receive payment
|
|
254
|
+
const addressResult = await deriveDepositAddresses.execute(ctx, {
|
|
255
|
+
prefix: '1sat',
|
|
256
|
+
count: 1,
|
|
257
|
+
})
|
|
258
|
+
const payAddress = addressResult.derivations[0]?.address
|
|
259
|
+
if (!payAddress) {
|
|
260
|
+
fatal('Failed to derive pay address')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const result = await listOrdinal.execute(ctx, {
|
|
264
|
+
ordinal,
|
|
265
|
+
price,
|
|
266
|
+
payAddress,
|
|
267
|
+
inputBEEF: ordinalsResult.BEEF as number[] | undefined,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if (result.error) {
|
|
271
|
+
fatal(result.error)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
275
|
+
} finally {
|
|
276
|
+
await destroy()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function ordinalsCancel(
|
|
281
|
+
args: string[],
|
|
282
|
+
opts: GlobalFlags,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const outpoint = extractFlag(args, '--outpoint')
|
|
285
|
+
|
|
286
|
+
if (!outpoint) fatal('Missing --outpoint <txid.vout>')
|
|
287
|
+
|
|
288
|
+
if (!opts.yes) {
|
|
289
|
+
const ok = await confirm({
|
|
290
|
+
message: `Cancel listing for ordinal ${outpoint}?`,
|
|
291
|
+
})
|
|
292
|
+
if (isCancel(ok) || !ok) {
|
|
293
|
+
fatal('Cancellation cancelled.')
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const privateKey = await loadKey(resolvePassword())
|
|
298
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
299
|
+
chain: opts.chain,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Look up the listing from the wallet (listings are in ordinals basket with ordlock tag)
|
|
304
|
+
const ordinalsResult = await getOrdinals.execute(ctx, { limit: 10000 })
|
|
305
|
+
const listing = ordinalsResult.outputs.find((o) => o.outpoint === outpoint)
|
|
306
|
+
if (!listing) {
|
|
307
|
+
fatal(`Listing not found in wallet: ${outpoint}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const result = await cancelListing.execute(ctx, {
|
|
311
|
+
listing,
|
|
312
|
+
inputBEEF: ordinalsResult.BEEF as number[] | undefined,
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
if (result.error) {
|
|
316
|
+
fatal(result.error)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
320
|
+
} finally {
|
|
321
|
+
await destroy()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function ordinalsBuy(args: string[], opts: GlobalFlags): Promise<void> {
|
|
326
|
+
const outpoint = extractFlag(args, '--outpoint')
|
|
327
|
+
|
|
328
|
+
if (!outpoint) fatal('Missing --outpoint <txid.vout>')
|
|
329
|
+
|
|
330
|
+
if (!opts.yes) {
|
|
331
|
+
const ok = await confirm({
|
|
332
|
+
message: `Purchase ordinal listing ${outpoint}?`,
|
|
333
|
+
})
|
|
334
|
+
if (isCancel(ok) || !ok) {
|
|
335
|
+
fatal('Purchase cancelled.')
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const privateKey = await loadKey(resolvePassword())
|
|
340
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
341
|
+
chain: opts.chain,
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const result = await purchaseOrdinal.execute(ctx, { outpoint })
|
|
346
|
+
|
|
347
|
+
if (result.error) {
|
|
348
|
+
fatal(result.error)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
352
|
+
} finally {
|
|
353
|
+
await destroy()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social commands - post.
|
|
3
|
+
*
|
|
4
|
+
* On-chain social protocol (BSocial) actions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createSocialPost } from '@1sat/actions'
|
|
8
|
+
import type { GlobalFlags } from '../args'
|
|
9
|
+
import { extractFlag } from '../args'
|
|
10
|
+
import { loadContext } from '../context'
|
|
11
|
+
import { printCommandHelp } from '../help'
|
|
12
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
13
|
+
import { fatal, output } from '../output'
|
|
14
|
+
|
|
15
|
+
export async function handleSocialCommand(
|
|
16
|
+
args: string[],
|
|
17
|
+
opts: GlobalFlags,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const [subcommand, ...rest] = args
|
|
20
|
+
|
|
21
|
+
switch (subcommand) {
|
|
22
|
+
case 'post':
|
|
23
|
+
return socialPost(rest, opts)
|
|
24
|
+
default:
|
|
25
|
+
printCommandHelp('social', {
|
|
26
|
+
post: 'Create an on-chain social post (--content <text> --app <name>)',
|
|
27
|
+
})
|
|
28
|
+
if (subcommand && subcommand !== 'help') {
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function socialPost(args: string[], opts: GlobalFlags): Promise<void> {
|
|
35
|
+
const content = extractFlag(args, '--content')
|
|
36
|
+
const app = extractFlag(args, '--app') ?? '1sat-cli'
|
|
37
|
+
|
|
38
|
+
if (!content) fatal('Missing --content <text>')
|
|
39
|
+
|
|
40
|
+
const privateKey = await loadKey(resolvePassword())
|
|
41
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
42
|
+
chain: opts.chain,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await createSocialPost.execute(ctx, { app, content })
|
|
47
|
+
|
|
48
|
+
if (result.error) {
|
|
49
|
+
fatal(result.error)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
53
|
+
} finally {
|
|
54
|
+
await destroy()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sweep commands - scan, import.
|
|
3
|
+
*
|
|
4
|
+
* Sweep assets from external wallets into the BRC-100 wallet.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GlobalFlags } from '../args'
|
|
8
|
+
import { extractFlag } from '../args'
|
|
9
|
+
import { printCommandHelp } from '../help'
|
|
10
|
+
import { fatal } from '../output'
|
|
11
|
+
|
|
12
|
+
export async function handleSweepCommand(
|
|
13
|
+
args: string[],
|
|
14
|
+
opts: GlobalFlags,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const [subcommand, ...rest] = args
|
|
17
|
+
|
|
18
|
+
switch (subcommand) {
|
|
19
|
+
case 'scan':
|
|
20
|
+
return sweepScan(rest, opts)
|
|
21
|
+
case 'import':
|
|
22
|
+
return sweepImport(rest, opts)
|
|
23
|
+
default:
|
|
24
|
+
printCommandHelp('sweep', {
|
|
25
|
+
scan: 'Scan an address for UTXOs (--address <addr>)',
|
|
26
|
+
import: 'Import UTXOs into wallet (--wif <key>)',
|
|
27
|
+
})
|
|
28
|
+
if (subcommand && subcommand !== 'help') {
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function sweepScan(args: string[], _opts: GlobalFlags): Promise<void> {
|
|
35
|
+
const address = extractFlag(args, '--address')
|
|
36
|
+
|
|
37
|
+
if (!address) fatal('Missing --address <addr>')
|
|
38
|
+
|
|
39
|
+
// TODO: Load context via loadContext() + loadKey()
|
|
40
|
+
// TODO: Use services to scan address for UTXOs (BSV, ordinals, tokens)
|
|
41
|
+
// TODO: Display found UTXOs with type classification
|
|
42
|
+
fatal('sweep scan is not yet implemented')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sweepImport(args: string[], _opts: GlobalFlags): Promise<void> {
|
|
46
|
+
const wif = extractFlag(args, '--wif')
|
|
47
|
+
|
|
48
|
+
if (!wif) fatal('Missing --wif <private-key>')
|
|
49
|
+
|
|
50
|
+
// TODO: Load context via loadContext() + loadKey()
|
|
51
|
+
// TODO: Call sweepBsv/sweepOrdinals/sweepBsv21 actions
|
|
52
|
+
// TODO: Output sweep results: txids and amounts imported
|
|
53
|
+
fatal('sweep import is not yet implemented')
|
|
54
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token commands - balances, list, send, deploy, buy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getBsv21Balances, listTokens, sendBsv21 } from '@1sat/actions'
|
|
6
|
+
import { confirm, isCancel } from '@clack/prompts'
|
|
7
|
+
import type { GlobalFlags } from '../args'
|
|
8
|
+
import { extractFlag } from '../args'
|
|
9
|
+
import { loadContext } from '../context'
|
|
10
|
+
import { printCommandHelp } from '../help'
|
|
11
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
12
|
+
import { fatal, formatLabel, formatValue, output } from '../output'
|
|
13
|
+
|
|
14
|
+
export async function handleTokensCommand(
|
|
15
|
+
args: string[],
|
|
16
|
+
opts: GlobalFlags,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
const [subcommand, ...rest] = args
|
|
19
|
+
|
|
20
|
+
switch (subcommand) {
|
|
21
|
+
case 'balances':
|
|
22
|
+
return tokenBalances(rest, opts)
|
|
23
|
+
case 'list':
|
|
24
|
+
return tokenList(rest, opts)
|
|
25
|
+
case 'send':
|
|
26
|
+
return tokenSend(rest, opts)
|
|
27
|
+
case 'deploy':
|
|
28
|
+
return tokenDeploy(rest, opts)
|
|
29
|
+
case 'buy':
|
|
30
|
+
return tokenBuy(rest, opts)
|
|
31
|
+
default:
|
|
32
|
+
printCommandHelp('tokens', {
|
|
33
|
+
balances: 'Show token balances by token ID',
|
|
34
|
+
list: 'List owned token UTXOs (--token-id <id>)',
|
|
35
|
+
send: 'Transfer tokens (--token-id <id> --to <addr> --amount <n>)',
|
|
36
|
+
deploy: 'Deploy a new BSV21 token (--symbol <sym> --amount <n>)',
|
|
37
|
+
buy: 'Purchase listed tokens (--outpoint <op>)',
|
|
38
|
+
})
|
|
39
|
+
if (subcommand && subcommand !== 'help') {
|
|
40
|
+
process.exit(1)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function tokenBalances(
|
|
46
|
+
_args: string[],
|
|
47
|
+
opts: GlobalFlags,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const privateKey = await loadKey(resolvePassword())
|
|
50
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
51
|
+
chain: opts.chain,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const balances = await getBsv21Balances.execute(ctx, {})
|
|
56
|
+
|
|
57
|
+
if (opts.json) {
|
|
58
|
+
output(balances, opts)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (balances.length === 0) {
|
|
63
|
+
output('No token balances found.', opts)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const b of balances) {
|
|
68
|
+
const symbol = b.sym ?? b.id.slice(0, 12)
|
|
69
|
+
console.log(
|
|
70
|
+
` ${formatValue(symbol)} ${formatLabel('amount:')} ${formatValue(b.amt)} ${formatLabel('id:')} ${b.id} ${formatLabel('dec:')} ${b.dec}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
await destroy()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function tokenList(args: string[], opts: GlobalFlags): Promise<void> {
|
|
79
|
+
const tokenId = extractFlag(args, '--token-id')
|
|
80
|
+
|
|
81
|
+
const privateKey = await loadKey(resolvePassword())
|
|
82
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
83
|
+
chain: opts.chain,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const outputs = await listTokens.execute(ctx, { limit: 10000 })
|
|
88
|
+
|
|
89
|
+
const filtered = tokenId
|
|
90
|
+
? outputs.filter((o) => {
|
|
91
|
+
const idTag = o.tags?.find((t) => t.startsWith('id:'))
|
|
92
|
+
return idTag && idTag.slice(3) === tokenId
|
|
93
|
+
})
|
|
94
|
+
: outputs
|
|
95
|
+
|
|
96
|
+
if (opts.json) {
|
|
97
|
+
output(filtered, opts)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (filtered.length === 0) {
|
|
102
|
+
output(
|
|
103
|
+
tokenId
|
|
104
|
+
? `No token UTXOs found for token ${tokenId}.`
|
|
105
|
+
: 'No token UTXOs found.',
|
|
106
|
+
opts,
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const o of filtered) {
|
|
112
|
+
const idTag =
|
|
113
|
+
o.tags?.find((t) => t.startsWith('id:'))?.slice(3) ?? 'unknown'
|
|
114
|
+
const amtTag = o.tags?.find((t) => t.startsWith('amt:'))?.slice(4) ?? '0'
|
|
115
|
+
const symTag = o.tags?.find((t) => t.startsWith('sym:'))?.slice(4) ?? ''
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
` ${formatValue(o.outpoint)} ${formatLabel(symTag || idTag.slice(0, 12))} ${formatLabel('amt:')} ${formatValue(amtTag)}`,
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`\n ${filtered.length} token UTXO(s) found.`)
|
|
123
|
+
} finally {
|
|
124
|
+
await destroy()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function tokenSend(args: string[], opts: GlobalFlags): Promise<void> {
|
|
129
|
+
const tokenId = extractFlag(args, '--token-id')
|
|
130
|
+
const to = extractFlag(args, '--to')
|
|
131
|
+
const amountStr = extractFlag(args, '--amount')
|
|
132
|
+
|
|
133
|
+
if (!tokenId) fatal('Missing --token-id <id>')
|
|
134
|
+
if (!to) fatal('Missing --to <address>')
|
|
135
|
+
if (!amountStr) fatal('Missing --amount <number>')
|
|
136
|
+
|
|
137
|
+
const amount = BigInt(amountStr)
|
|
138
|
+
if (amount <= 0n) {
|
|
139
|
+
fatal('--amount must be a positive number')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!opts.yes) {
|
|
143
|
+
const ok = await confirm({
|
|
144
|
+
message: `Send ${amountStr} tokens (${tokenId.slice(0, 12)}...) to ${to}?`,
|
|
145
|
+
})
|
|
146
|
+
if (isCancel(ok) || !ok) {
|
|
147
|
+
fatal('Token send cancelled.')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const privateKey = await loadKey(resolvePassword())
|
|
152
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
153
|
+
chain: opts.chain,
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = await sendBsv21.execute(ctx, {
|
|
158
|
+
tokenId,
|
|
159
|
+
amount: amountStr,
|
|
160
|
+
address: to,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
if (result.error) {
|
|
164
|
+
fatal(result.error)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output(opts.json ? result : { txid: result.txid }, opts)
|
|
168
|
+
} finally {
|
|
169
|
+
await destroy()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function tokenDeploy(args: string[], _opts: GlobalFlags): Promise<void> {
|
|
174
|
+
const symbol = extractFlag(args, '--symbol')
|
|
175
|
+
const amountStr = extractFlag(args, '--amount')
|
|
176
|
+
const _icon = extractFlag(args, '--icon')
|
|
177
|
+
|
|
178
|
+
if (!symbol) fatal('Missing --symbol <sym>')
|
|
179
|
+
if (!amountStr) fatal('Missing --amount <initial-supply>')
|
|
180
|
+
|
|
181
|
+
// TODO: Call deployToken action
|
|
182
|
+
// TODO: Output txid and token origin
|
|
183
|
+
fatal('tokens deploy is not yet implemented')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function tokenBuy(args: string[], _opts: GlobalFlags): Promise<void> {
|
|
187
|
+
const outpoint = extractFlag(args, '--outpoint')
|
|
188
|
+
|
|
189
|
+
if (!outpoint) fatal('Missing --outpoint <txid.vout>')
|
|
190
|
+
|
|
191
|
+
// TODO: Call purchaseTokenListing action
|
|
192
|
+
// TODO: Output txid
|
|
193
|
+
fatal('tokens buy is not yet implemented')
|
|
194
|
+
}
|