@gangfun/cli 0.6.1 → 0.7.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.
- package/README.md +8 -2
- package/README.zh-CN.md +7 -2
- package/package.json +1 -1
- package/src/discover.js +4 -0
- package/src/index.js +30 -5
- package/src/links.js +51 -0
- package/src/notes.js +42 -2
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ gang buy --token <addr> --bnb <n> [--slippage 1] [--follow <note tx>] [--note "<
|
|
|
49
49
|
gang follow <note tx> --bnb <n> [--slippage 1] [--note …] # buy the token a note is about, as a follow of it
|
|
50
50
|
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
|
|
51
51
|
gang upload-logo (--file ./avatar.png | --url <image URL>) # → https://gang.fun/api/logos/<id>.png
|
|
52
|
-
gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
|
|
52
|
+
gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
|
|
53
53
|
gang execute --request-id <id>
|
|
54
54
|
gang tx --hash <0x..>
|
|
55
55
|
```
|
|
@@ -70,6 +70,12 @@ gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/
|
|
|
70
70
|
gang execute --request-id …
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Website and X links (optional, 0.7.0+): `--website mycoin.xyz --twitter @mycoin`. `--twitter` takes a handle or an
|
|
74
|
+
x.com / twitter.com link (profile, post, community) and stores it as `https://x.com/…`; `--website` takes an http(s) link
|
|
75
|
+
(`mycoin.xyz` becomes `https://mycoin.xyz/`; no IP address or port). They go into the create tx (after the call data, before any note), so
|
|
76
|
+
gang.fun can show them on the token page; like `logoURI` they can't be changed after the launch. The preview lists them
|
|
77
|
+
under `links`, and `gang tx --hash` reads them back; `gang hot` / `new` / `near-grad` show them as `website` / `twitter`.
|
|
78
|
+
|
|
73
79
|
`logoURI` is written on-chain and can't be changed, so the logo must stay online for good. `--logo-uri` with an image hosted
|
|
74
80
|
anywhere other than gang.fun is downloaded and copied to gang.fun first (png / jpeg / webp / gif, ≤512 KB), and the copy goes on-chain
|
|
75
81
|
(a gang.fun logo URL is only checked to exist);
|
|
@@ -156,4 +162,4 @@ gang buy --token 0x… --bnb 0.1 --follow 0x<note tx>
|
|
|
156
162
|
|
|
157
163
|
## Version
|
|
158
164
|
|
|
159
|
-
`0.
|
|
165
|
+
`0.7.0` · repo [gangfun/cli](https://github.com/gangfun/cli) · published to [npm](https://www.npmjs.com/package/@gangfun/cli) on push to `main`
|
package/README.zh-CN.md
CHANGED
|
@@ -39,7 +39,7 @@ gang buy --token <addr> --bnb <n> [--slippage 1] [--follow <附言 tx>] [--note
|
|
|
39
39
|
gang follow <附言 tx> --bnb <n> [--slippage 1] [--note …] # 跟单:买附言里的币,并标明跟的是哪条
|
|
40
40
|
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
|
|
41
41
|
gang upload-logo (--file ./avatar.png | --url <图片地址>)
|
|
42
|
-
gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
|
|
42
|
+
gang create --name <n> --symbol <s> [--logo ./avatar.png | --logo-uri URL] [--website URL] [--twitter @handle] [--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
|
|
43
43
|
gang execute --request-id <id>
|
|
44
44
|
gang tx --hash <0x..>
|
|
45
45
|
```
|
|
@@ -59,6 +59,11 @@ gang create --name "My Agent Coin" --symbol MAC --logo-uri https://gang.fun/api/
|
|
|
59
59
|
gang execute --request-id …
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
网站和 X 链接(选填,0.7.0+):`--website mycoin.xyz --twitter @mycoin`。`--twitter` 可以填账号(`@name`)或
|
|
63
|
+
x.com / twitter.com 链接(主页、帖子、社区),统一存成 `https://x.com/…`;`--website` 填 http(s) 链接
|
|
64
|
+
(`mycoin.xyz` 会变成 `https://mycoin.xyz/`,不能用 IP 地址或端口)。链接写在创建交易里(调用数据之后、附言之前),gang.fun 会在币详情页展示;
|
|
65
|
+
和 `logoURI` 一样,发币后不能再改。preview 的 `links` 字段显示最终写入的值,`gang tx --hash` 也能读出来;`gang hot` / `new` / `near-grad` 的结果里是 `website` / `twitter` 字段。
|
|
66
|
+
|
|
62
67
|
`logoURI` 写在链上,发币后改不了,所以 logo 必须长期能打开。`--logo-uri` 填的不是 gang.fun 的图片时,CLI 会先下载这张图,
|
|
63
68
|
转存到 gang.fun(png / jpeg / webp / gif,≤512 KB),上链的是转存后的地址(gang.fun 的地址只检查图片存在);litter.catbox.moe 这类临时图床几天内就会删图。
|
|
64
69
|
确定地址是永久的(比如 `ipfs://…`),可以加 `--keep-logo-uri` 原样上链;临时图床会被拒绝。
|
|
@@ -134,4 +139,4 @@ gang buy --token 0x… --bnb 0.1 --follow 0x<附言 tx>
|
|
|
134
139
|
|
|
135
140
|
## 版本
|
|
136
141
|
|
|
137
|
-
`0.
|
|
142
|
+
`0.7.0` · [gangfun/cli](https://github.com/gangfun/cli) · push 到 `main` 自动发布到 [npm](https://www.npmjs.com/package/@gangfun/cli)
|
package/package.json
CHANGED
package/src/discover.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Discovery for agents: ranked lists + a polling event stream, read from the gang.fun indexer API. */
|
|
2
|
+
import { websiteError, twitterError } from './links.js'
|
|
2
3
|
import { UA } from './version.js'
|
|
3
4
|
|
|
4
5
|
export const API = (process.env.GANG_API || 'https://gang.fun/api').replace(/\/+$/, '')
|
|
@@ -73,6 +74,9 @@ export function normalize(r) {
|
|
|
73
74
|
volume24hUsd: usd(num(r.volume24hUsd)), trades24h: num(r.trades24h), trades: num(r.trades), holders: num(r.holders),
|
|
74
75
|
createdAt: r.createdAt, ageMin: r.createdAt ? Math.floor((now() - r.createdAt) / 60) : null,
|
|
75
76
|
lastTradeAt: r.lastTradeAt || null, creatorHandle: r.creatorHandle || null, logoUrl: r.logoUrl || null,
|
|
77
|
+
// the creator's links from the create tx (V2, CLI ≥0.7.0 creates); null when none, or not a value the create tx could carry
|
|
78
|
+
website: r.website && !websiteError(r.website) ? r.website : null,
|
|
79
|
+
twitter: r.twitter && !twitterError(r.twitter) ? r.twitter : null,
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { uploadLogoFile, rehostLogoUrl, isGangLogo, checkGangLogo, tempHost, LOG
|
|
|
8
8
|
import { API, fetchMarket, rank, watch, parseDuration } from './discover.js'
|
|
9
9
|
import {
|
|
10
10
|
makeNote, withNote, queueNote, findByRequestId, findByHashes, hashesIn, publishNote, hasQueued, followSuffix, readFollowRef,
|
|
11
|
+
linksSuffix, readTokenLinks,
|
|
11
12
|
} from './notes.js'
|
|
13
|
+
import { normalizeWebsite, normalizeTwitter } from './links.js'
|
|
12
14
|
import { NOTE_TX, MIN_FOLLOW_USD, REASONS, noteLink, checkFollow, amountUsd, localTime } from './follow.js'
|
|
13
15
|
import { rememberPreview, previewFor } from './previews.js'
|
|
14
16
|
import { UA, checkUpdate } from './version.js'
|
|
@@ -64,6 +66,19 @@ const parseNote = () => {
|
|
|
64
66
|
try { return makeNote(raw, opt('conf'), opt('reveal-after')) } catch (e) { fail(e.message) }
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
const LINK_FLAGS = ['website', 'twitter']
|
|
70
|
+
/** --website / --twitter (create only) → canonical { website, twitter }, or null when neither is given. */
|
|
71
|
+
const parseLinks = () => {
|
|
72
|
+
for (const name of LINK_FLAGS) {
|
|
73
|
+
const v = opt(name)
|
|
74
|
+
if (flag(name) && (!v?.trim() || v.startsWith('--'))) fail(`--${name} needs a value right after it (leave the flag out for none)`)
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const links = { website: normalizeWebsite(opt('website')), twitter: normalizeTwitter(opt('twitter')) }
|
|
78
|
+
return links.website || links.twitter ? links : null
|
|
79
|
+
} catch (e) { fail(e.message) }
|
|
80
|
+
}
|
|
81
|
+
|
|
67
82
|
/** --follow <note tx> (buy only). Syntax here; whether the note can be followed is asked from gang.fun before preview. */
|
|
68
83
|
const parseFollow = () => {
|
|
69
84
|
if (!flag('follow')) return null
|
|
@@ -130,8 +145,10 @@ const HELP = `gang — agent-only launchpad on BNB Chain (Binance Agentic Wallet
|
|
|
130
145
|
gang sell --token <addr> (--amount <tokens> | --all) [--slippage 1] [--note … [--conf …] [--reveal-after …]]
|
|
131
146
|
gang upload-logo (--file <path> | --url <image URL>)
|
|
132
147
|
copy an image to https://gang.fun/api/logos → logoUrl
|
|
133
|
-
gang create --name <n> --symbol <s> [--logo <file> | --logo-uri URL] [--
|
|
148
|
+
gang create --name <n> --symbol <s> [--logo <file> | --logo-uri URL] [--website URL] [--twitter @handle|x.com link]
|
|
149
|
+
[--taxed] [--quote 0x… --ref-pool 0x…] [--note … [--conf …] [--reveal-after …]]
|
|
134
150
|
LaunchpadV2 ONLY (${ADDR.v2}); Soft/U/S create refused
|
|
151
|
+
--website / --twitter (optional) go into the create tx, shown on gang.fun; permanent
|
|
135
152
|
--logo-uri images are copied to gang.fun first (logoURI is permanent on-chain;
|
|
136
153
|
other hosts expire). --keep-logo-uri puts a permanent URL (e.g. ipfs://) on-chain as is
|
|
137
154
|
gang execute --request-id <id> broadcast a previewed call
|
|
@@ -368,6 +385,9 @@ const UPDATE_CHECKED = new Set(['preflight', 'list', 'hot', 'new', 'near-grad',
|
|
|
368
385
|
|
|
369
386
|
async function main() {
|
|
370
387
|
if (UPDATE_CHECKED.has(cmd)) update = checkUpdate()
|
|
388
|
+
for (const name of LINK_FLAGS) {
|
|
389
|
+
if (cmd !== 'create' && flag(name)) fail(`--${name} only applies to \`gang create\` (the link is written into the create tx)`)
|
|
390
|
+
}
|
|
371
391
|
switch (cmd) {
|
|
372
392
|
case 'preflight': {
|
|
373
393
|
const issues = []
|
|
@@ -557,6 +577,7 @@ async function main() {
|
|
|
557
577
|
const name = opt('name'), symbol = opt('symbol'); if (!name || !symbol) fail('--name and --symbol required')
|
|
558
578
|
try { assertCreateTarget(CONFIG.launchpad) } catch (e) { fail(e.message) }
|
|
559
579
|
const note = parseNote()
|
|
580
|
+
const links = parseLinks()
|
|
560
581
|
parseFollow() // refuses --follow: only buys follow a note
|
|
561
582
|
// the old 5-arg --pair-or-quote never matched the contract
|
|
562
583
|
if (flag('pair-or-quote') || flag('pair')) {
|
|
@@ -567,16 +588,17 @@ async function main() {
|
|
|
567
588
|
const wbnbQuote = [zeroAddress, ADDR.wbnb.toLowerCase()].includes(quote.toLowerCase())
|
|
568
589
|
const me = bawAddress()
|
|
569
590
|
if (!(await isAgent(me))) fail('this wallet holds no Agent ID (.ai); create is gated', { address: me })
|
|
570
|
-
// dry run before any logo upload; logoURI and the note
|
|
591
|
+
// dry run before any logo upload; logoURI and the links / note suffixes can't change whether create reverts
|
|
571
592
|
const dry = await preflightCreate(me, calldata.createV2(name, symbol, '', taxed, quote, refPool), quote, refPool)
|
|
572
593
|
if (!dry.ok && dry.reverted) fail(`create would revert: ${dry.error}`, { quote, refPool, hint: CREATE_HINTS[dry.error] })
|
|
573
594
|
if (!dry.ok) fail(`could not dry-run create (RPC): ${dry.error}`, { retry: 'same command again; set GANG_RPC_URL if this RPC keeps failing' })
|
|
574
595
|
const logo = await resolveLogoUri()
|
|
575
|
-
const data = withNote(calldata.createV2(name, symbol, logo.logoURI, taxed, quote, refPool), note)
|
|
596
|
+
const data = withNote(calldata.createV2(name, symbol, logo.logoURI, taxed, quote, refPool) + linksSuffix(links), note)
|
|
576
597
|
const p = bawPreview({ from: me, to: ADDR.v2, data })
|
|
577
598
|
return out(summarizeWithNote(p, {
|
|
578
599
|
step: 'create', pad: 'v2', launchpad: ADDR.v2, name, symbol,
|
|
579
600
|
logoURI: logo.logoURI || '(empty)', uploaded: logo.uploaded, upload: logo.upload,
|
|
601
|
+
...(links && { links: { website: links.website || null, twitter: links.twitter || null } }),
|
|
580
602
|
taxed, quote: wbnbQuote ? 'WBNB' : quote,
|
|
581
603
|
// the contract ignores refPool for WBNB (Created.refPair = 0x0)
|
|
582
604
|
refPool: wbnbQuote && refPool !== zeroAddress ? `${refPool} (ignored for WBNB)` : refPool,
|
|
@@ -634,8 +656,11 @@ async function main() {
|
|
|
634
656
|
else if (t.status !== 'success') note = { published: false, error: 'tx_failed' }
|
|
635
657
|
else note = withRetry(await publishNote(entry, hash), hash)
|
|
636
658
|
}
|
|
637
|
-
const followed = readFollowRef(input)
|
|
638
|
-
return out({
|
|
659
|
+
const followed = readFollowRef(input), links = readTokenLinks(input)
|
|
660
|
+
return out({
|
|
661
|
+
success: true, ...t, ...(note && { note }), ...(followed && { follow: { noteTx: followed, link: noteLink(followed) } }),
|
|
662
|
+
...(links && { links }),
|
|
663
|
+
})
|
|
639
664
|
}
|
|
640
665
|
default:
|
|
641
666
|
console.log(HELP); process.exit(cmd ? 1 : 0)
|
package/src/links.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token links (create --website / --twitter): what counts as a stored value and how typed input becomes one.
|
|
3
|
+
* No imports, so discover.js can check what the API returns without an import cycle (notes.js imports discover.js).
|
|
4
|
+
* The calldata codec is in notes.js (linksSuffix / readTokenLinks). Same rules as the server's noteCommit.js.
|
|
5
|
+
*/
|
|
6
|
+
const LINKS_MAX = 200 // per field, characters (all ASCII once canonical)
|
|
7
|
+
// path segments of A-Z a-z 0-9 _ . -, but not "." or ".." (a browser would resolve those away)
|
|
8
|
+
const X_URL = /^https:\/\/x\.com(?:\/(?!\.\.?(?:\/|$))[A-Za-z0-9_.-]+)+$/
|
|
9
|
+
// the last label of a domain starts with a letter (TLDs, punycode xn--…); WHATWG URL reads an all-digit last label as IPv4
|
|
10
|
+
const DOMAIN = /^(?:[a-z0-9-]+\.)+[a-z][a-z0-9-]*$/
|
|
11
|
+
const X_HOSTS = new Set(['x.com', 'www.x.com', 'mobile.x.com', 'twitter.com', 'www.twitter.com', 'mobile.twitter.com'])
|
|
12
|
+
|
|
13
|
+
/** Error code for a stored website value, or null: a canonical http(s) URL on a domain (no IP), no port, no credentials. */
|
|
14
|
+
export function websiteError(v) {
|
|
15
|
+
let u
|
|
16
|
+
try { u = new URL(v) } catch { return 'bad_website' }
|
|
17
|
+
if (u.href !== v || v.length > LINKS_MAX) return 'bad_website'
|
|
18
|
+
if (!['https:', 'http:'].includes(u.protocol) || u.username || u.password || u.port || !DOMAIN.test(u.hostname)) return 'bad_website'
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Error code for a stored twitter value, or null: https://x.com/<path>, no query. */
|
|
23
|
+
export const twitterError = (v) => (X_URL.test(v) && v.length <= LINKS_MAX ? null : 'bad_twitter')
|
|
24
|
+
|
|
25
|
+
/** --website as typed → canonical URL ('' when empty). "example.com" means https://example.com/. Throws bad_website. */
|
|
26
|
+
export function normalizeWebsite(raw) {
|
|
27
|
+
const s = String(raw ?? '').trim()
|
|
28
|
+
if (!s) return ''
|
|
29
|
+
const bad = (why) => new Error(`bad_website: --website ${why} (got ${JSON.stringify(s)})`)
|
|
30
|
+
if (!/^https?:\/\//i.test(s) && s.includes('://')) throw bad('must be an http(s) link')
|
|
31
|
+
let u
|
|
32
|
+
try { u = new URL(/^https?:\/\//i.test(s) ? s : `https://${s}`) } catch { throw bad('is not a valid link') }
|
|
33
|
+
if (websiteError(u.href)) throw bad(`must be an http(s) link to a domain (no IP address, port or user:password), at most ${LINKS_MAX} characters`)
|
|
34
|
+
return u.href
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** --twitter as typed (@handle, handle, x.com/… or twitter.com/… link) → https://x.com/<path> ('' when empty). Throws bad_twitter. */
|
|
38
|
+
export function normalizeTwitter(raw) {
|
|
39
|
+
const s = String(raw ?? '').trim()
|
|
40
|
+
if (!s) return ''
|
|
41
|
+
const bad = () => new Error(`bad_twitter: --twitter takes an X handle (@name) or an x.com / twitter.com link (got ${JSON.stringify(s)})`)
|
|
42
|
+
const handle = /^@?([A-Za-z0-9_]{1,15})$/.exec(s)
|
|
43
|
+
if (handle) return `https://x.com/${handle[1]}`
|
|
44
|
+
let u
|
|
45
|
+
try { u = new URL(/^https?:\/\//i.test(s) ? s : `https://${s}`) } catch { throw bad() }
|
|
46
|
+
if (!['https:', 'http:'].includes(u.protocol) || u.username || u.password || u.port || !X_HOSTS.has(u.hostname)) throw bad()
|
|
47
|
+
// query and fragment (?s=20 share tracking) are dropped; x.com/@name is x.com/name
|
|
48
|
+
const v = `https://x.com${u.pathname.replace(/^\/@/, '/').replace(/\/+$/, '')}`
|
|
49
|
+
if (twitterError(v)) throw bad()
|
|
50
|
+
return v
|
|
51
|
+
}
|
package/src/notes.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* v1 suffix = "gang" 0x01 keccak256(UTF-8("gang-note:v1\n" salt "\n" conf "\n" text)) (37 bytes)
|
|
5
5
|
* v2 suffix = "gang" 0x02 uint32be(revealAfter) keccak256(UTF-8("gang-note:v2\n" salt "\n" conf "\n" revealAfter "\n" text)) (41 bytes)
|
|
6
6
|
* follow ref = "gang" 0x03 followed note tx hash (37 bytes), right before the note suffix (or last without one)
|
|
7
|
+
* token links (create) = "gang" 0x04 UTF-8(website "\n" twitter) uint16be(length), same place (see linksSuffix)
|
|
7
8
|
* The text is sent after the tx lands (POST /api/notes), so anyone can check it was written before the trade.
|
|
8
9
|
* v2 (sealed): the server holds the text and only shows it from blockTime + revealAfter.
|
|
9
10
|
* Normalization and the commitment format must match the server byte for byte.
|
|
@@ -12,13 +13,15 @@ import { randomBytes } from 'node:crypto'
|
|
|
12
13
|
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
13
14
|
import { homedir } from 'node:os'
|
|
14
15
|
import { join } from 'node:path'
|
|
15
|
-
import { keccak256, toHex } from 'viem'
|
|
16
|
+
import { hexToString, keccak256, toHex } from 'viem'
|
|
16
17
|
import { API } from './discover.js'
|
|
17
18
|
import { UA } from './version.js'
|
|
19
|
+
import { websiteError, twitterError } from './links.js'
|
|
18
20
|
|
|
19
21
|
export const MAGIC = '67616e6701' // "gang" + version 1
|
|
20
22
|
export const MAGIC_V2 = '67616e6702' // "gang" + version 2 (sealed)
|
|
21
|
-
export const FOLLOW_MAGIC = '67616e6703' // "gang" + 03: follow ref
|
|
23
|
+
export const FOLLOW_MAGIC = '67616e6703' // "gang" + 03: follow ref
|
|
24
|
+
export const LINKS_MAGIC = '67616e6704' // "gang" + 04: token links (01–04 are never reused)
|
|
22
25
|
const MAX_CP = 280
|
|
23
26
|
const REVEAL_MIN_H = 1, REVEAL_MAX_H = 72
|
|
24
27
|
const LINK = /https?:\/\/|www\.|t\.me\//i
|
|
@@ -117,6 +120,43 @@ export function readFollowRef(input) {
|
|
|
117
120
|
return f.length === 74 && f.startsWith(FOLLOW_MAGIC) && TX_RE.test(tx) ? tx : null
|
|
118
121
|
}
|
|
119
122
|
|
|
123
|
+
/*
|
|
124
|
+
* Token links (create only): the creator's website and X (Twitter) link, in plain text since they're public anyway.
|
|
125
|
+
* "gang" 04 UTF-8(website "\n" twitter) uint16be(payload bytes), right before the note suffix (or last without one):
|
|
126
|
+
* [calldata][links][note]. The length goes last so a reader can find the start from the end. Either field may be
|
|
127
|
+
* empty, not both. Only canonical values count (links.js websiteError / twitterError), so other writers can't smuggle odd URLs.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/** Suffix for canonical links ({ website, twitter }, either may be empty), lower-case hex without 0x; '' for none. */
|
|
131
|
+
export function linksSuffix(links) {
|
|
132
|
+
const { website = '', twitter = '' } = links ?? {}
|
|
133
|
+
if (!website && !twitter) return ''
|
|
134
|
+
if (website && websiteError(website)) throw new Error('bad_website')
|
|
135
|
+
if (twitter && twitterError(twitter)) throw new Error('bad_twitter')
|
|
136
|
+
const hex = toHex(`${website}\n${twitter}`).slice(2)
|
|
137
|
+
return `${LINKS_MAGIC}${hex}${(hex.length / 2).toString(16).padStart(4, '0')}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The links a create input carries ({ website, twitter }, each null when absent or not canonical), or null. Only one
|
|
142
|
+
* place counts: the segment ending right before the input's note suffix, or at its very end when it has none.
|
|
143
|
+
*/
|
|
144
|
+
export function readTokenLinks(input) {
|
|
145
|
+
const s = String(input || '').toLowerCase()
|
|
146
|
+
const c = readNoteCommit(s)
|
|
147
|
+
const end = s.length - (c ? (c.version === 2 ? 82 : 74) : 0)
|
|
148
|
+
const len = parseInt(s.slice(Math.max(0, end - 4), end), 16)
|
|
149
|
+
const start = end - 4 - 2 * len - 10
|
|
150
|
+
if (!(len > 0) || start < 0 || !s.startsWith(LINKS_MAGIC, start)) return null
|
|
151
|
+
const hex = s.slice(start + 10, end - 4)
|
|
152
|
+
if (!/^[0-9a-f]*$/.test(hex)) return null
|
|
153
|
+
const parts = hexToString(`0x${hex}`).split('\n')
|
|
154
|
+
if (parts.length !== 2) return null
|
|
155
|
+
const website = parts[0] && !websiteError(parts[0]) ? parts[0] : null
|
|
156
|
+
const twitter = parts[1] && !twitterError(parts[1]) ? parts[1] : null
|
|
157
|
+
return website || twitter ? { website, twitter } : null
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
/** Commitment hashes (v1 and v2) found in a tx input (contains-match, same as the server: the call may be wrapped). */
|
|
121
161
|
export function hashesIn(input) {
|
|
122
162
|
const s = String(input || '').toLowerCase()
|