Legacy API (V0)
The original registry's endpoints, at the same paths and in the same shapes. Kept for compatibility.
| Network | Base URL |
|---|---|
| Liquid | https://assets.blockstream.info |
| Liquid Testnet | https://assets-testnet.blockstream.info |
Examples use the Liquid (mainnet) base URL. For testnet, swap in the testnet base URL.
Endpoints
| Method | Endpoint | Path |
|---|---|---|
| POST | Register (legacy) | / |
| GET | List all (legacy) | /index.json |
| GET | Get an asset (legacy) | /:asset_id |
| GET | Get all icons (legacy) | /icons.json |
| POST | Validate a contract | /contract/validate |
| DELETE | Deregister (legacy) | /:asset_id |
Legacy endpoints
Register (legacy)
POST / LEGACY
Registers a version 0 contract. Same chain and domain checks as v2 (HTTP or DNS proof; no pubkey-bound proof). The asset can't use v2 issuer actions until it's migrated.
Parameters
| Name | In · type | Description |
|---|---|---|
asset_id required | body · hex (64) | The issued asset's ID. |
contract required | body · object | Version 0: entity.domain, issuer_pubkey, name, precision (0–8), optional ticker (3–24), version: 0. Extra fields allowed (≤ 32, ≤ 2,048 bytes each); whole contract ≤ 4,096 bytes. |
domain_verification_method | body · "http" | "dns" | Default http. |
Can return 400 with the full asset record even though the asset was registered. Check GET /:asset_id before retrying.
Example request
curl -X POST "https://assets.blockstream.info/" \
-H "Content-Type: application/json" \
-d '{"asset_id":"7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f","contract":{"entity":{"domain":"legacy-issuer.com"},"issuer_pubkey":"03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e","name":"Example Token","precision":0,"ticker":"EXTKN","version":0},"domain_verification_method":"http"}'// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const payload = {
asset_id: "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
contract: { entity: { domain: "legacy-issuer.com" }, issuer_pubkey: "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", name: "Example Token", precision: 0, ticker: "EXTKN", version: 0 },
domain_verification_method: "http",
};
const res = await fetch(`${BASE_URL}/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects# pip install requests
import requests
BASE_URL = "https://assets.blockstream.info"
payload = {
"asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
"contract": {"entity": {"domain": "legacy-issuer.com"}, "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", "name": "Example Token", "precision": 0, "ticker": "EXTKN", "version": 0},
"domain_verification_method": "http",
}
res = requests.post(f"{BASE_URL}/", json=payload, timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://assets.blockstream.info"
endpoint := baseURL + "/"
payload, err := json.Marshal(map[string]any{"asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f", "contract": map[string]any{"entity": map[string]any{"domain": "legacy-issuer.com"}, "issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e", "name": "Example Token", "precision": 0, "ticker": "EXTKN", "version": 0}, "domain_verification_method": "http"})
if err != nil {
panic(err)
}
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusCreated {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}Response: 201 (local instance · application/json)
{
"asset_id": "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f",
"contract": {
"version": 0,
"issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e",
"name": "Example Token",
"ticker": "EXTKN",
"precision": 0,
"entity": {
"domain": "legacy-issuer.com"
}
},
"version": 0,
"issuer_pubkey": "03566ecb682f1a49ccad62a6dbc5b4045d96d65912bbbb177b7121c27c01d91e6e",
"name": "Example Token",
"precision": 0,
"entity": {
"domain": "legacy-issuer.com"
},
"ticker": "EXTKN"
}List all (legacy)
GET /index.json LEGACY
Every active asset in the legacy shape, keyed by asset ID. Includes assets registered through v2.
Also available at GET / (same response).
Parameters
| Name | In · type | Description |
|---|---|---|
If-Modified-Since | header · HTTP date | The Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching. |
v2-registered assets appear with
version: 2, their key incontract.initial_issuer_pubkey, and no top-levelissuer_pubkey. See Migrating from legacy.
Example request
curl "https://assets.blockstream.info/index.json"// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const res = await fetch(`${BASE_URL}/index.json`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects# pip install requests
import requests
BASE_URL = "https://assets.blockstream.info"
res = requests.get(f"{BASE_URL}/index.json", timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())package main
import (
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://assets.blockstream.info"
endpoint := baseURL + "/index.json"
res, err := http.Get(endpoint)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}Response: 200 (production · application/json · 14,230 entries, first shown)
{
"aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034": {
"name": "BMN Series 2",
"entity": {
"domain": "blockstream-mining2.stokr.io"
},
"ticker": "BMN2",
"version": 0,
"asset_id": "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034",
"contract": {
"name": "BMN Series 2",
"entity": {
"domain": "blockstream-mining2.stokr.io"
},
"ticker": "BMN2",
"version": 0,
"precision": 4,
"issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"
},
"precision": 4,
"collection": null,
"issuance_txin": {
"txid": "9bd4592c4238146e3b116f0b307100e38d58a26c62465e53c0ba7b8511ce7c88",
"vin": 0
},
"issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b",
"issuance_prevout": {
"txid": "b9317d00e44ab9f4ce435677b9b3f915e09d12b2be648da6bd12cd790d12658d",
"vout": 3
}
}
}Get an asset (legacy)
GET /:asset_id LEGACY
One active asset in the legacy shape. Assets imported from the original registry keep their original fields, such as issuance_txin and issuance_prevout.
Parameters
| Name | In · type | Description |
|---|---|---|
asset_id required | path · hex (64) | Liquid asset ID. Upper-case hex is accepted and normalized. |
If-Modified-Since | header · HTTP date | The Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching. |
Example request
curl "https://assets.blockstream.info/aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const ASSET_ID = "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034";
const res = await fetch(`${BASE_URL}/${ASSET_ID}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects# pip install requests
import requests
BASE_URL = "https://assets.blockstream.info"
ASSET_ID = "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"
res = requests.get(f"{BASE_URL}/{ASSET_ID}", timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())package main
import (
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://assets.blockstream.info"
assetID := "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034"
endpoint := baseURL + "/" + assetID
res, err := http.Get(endpoint)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}Response: 200 (production · application/json)
{
"name": "BMN Series 2",
"entity": {
"domain": "blockstream-mining2.stokr.io"
},
"ticker": "BMN2",
"version": 0,
"asset_id": "aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034",
"contract": {
"name": "BMN Series 2",
"entity": {
"domain": "blockstream-mining2.stokr.io"
},
"ticker": "BMN2",
"version": 0,
"precision": 4,
"issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"
},
"precision": 4,
"collection": null,
"issuance_txin": {
"txid": "9bd4592c4238146e3b116f0b307100e38d58a26c62465e53c0ba7b8511ce7c88",
"vin": 0
},
"issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b",
"issuance_prevout": {
"txid": "b9317d00e44ab9f4ce435677b9b3f915e09d12b2be648da6bd12cd790d12658d",
"vout": 3
}
}A v2-registered asset in the legacy shape (testnet): 200 (production · application/json)
{
"asset_id": "a20caa0e4bff1a884d46e8b65571294569e041e8eee72104afc2269582d37853",
"contract": {
"entity": {
"domain": "liquidtestnet.com"
},
"name": "Blockstream Bond 2",
"precision": 2,
"version": 2,
"initial_issuer_pubkey": "02604044b90e208a4baf09ca37aa22b3775c81d80c717e81f642eb7bce922c5d7c",
"ticker": "B-Bond2"
},
"version": 2,
"name": "Blockstream Bond 2",
"precision": 2,
"entity": {
"domain": "liquidtestnet.com"
},
"ticker": "B-Bond2"
}Get all icons (legacy)
GET /icons.json LEGACY
Every approved icon as Base64 PNG, keyed by asset ID, in one response. Includes icons approved through the v2 workflow and the L-BTC icon.
Parameters
| Name | In · type | Description |
|---|---|---|
If-Modified-Since | header · HTTP date | The Last-Modified value from your previous response. Returns 304 with no body if no asset has changed since. Caching. |
Sent with
Cache-Control: no-cacheand the registry-wideLast-Modified, so any asset change, not only an icon change, makes it new. SendIf-Modified-Sinceto get a 304 instead of re-downloading the whole map.
Prefer each asset's
icon.href: immutable, cacheable, and fetched only when you need it.
Example request
curl "https://assets.blockstream.info/icons.json"// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const res = await fetch(`${BASE_URL}/icons.json`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2)); // full output: plain console.log cuts long strings and deep objects# pip install requests
import requests
BASE_URL = "https://assets.blockstream.info"
res = requests.get(f"{BASE_URL}/icons.json", timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.json())package main
import (
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://assets.blockstream.info"
endpoint := baseURL + "/icons.json"
res, err := http.Get(endpoint)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}Response: 200 (production · application/json · 1,059 entries, first shown)
{
"aa909f1b77451e409fe95fe1d3638ad017ab3325c6d4f00301af6d582d0f2034": "iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAYAAADL1t+KAAAA…"
}Validate a contract
POST /contract/validate LEGACY
Checks that a version 0 contract hashes to the given contract hash. Returns valid as plain text, or 409 with the expected hash. Writes nothing. Version 1 and 2 contracts are rejected (422); compute their hash yourself (Register an asset).
Parameters
| Name | In · type | Description |
|---|---|---|
contract required | body · object | Version 0 contract. |
contract_hash required | body · hex (64) | Hash you committed at issuance. |
Example request
curl -X POST "https://assets.blockstream.info/contract/validate" \
-H "Content-Type: application/json" \
-d '{"contract":{"name":"BMN Series 2","entity":{"domain":"blockstream-mining2.stokr.io"},"ticker":"BMN2","version":0,"precision":4,"issuer_pubkey":"0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"},"contract_hash":"998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"}'// Node.js 18+ (ES module: save as .mjs or set "type": "module")
const BASE_URL = "https://assets.blockstream.info";
const payload = {
contract: { name: "BMN Series 2", entity: { domain: "blockstream-mining2.stokr.io" }, ticker: "BMN2", version: 0, precision: 4, issuer_pubkey: "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b" },
contract_hash: "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
};
const res = await fetch(`${BASE_URL}/contract/validate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.text());# pip install requests
import requests
BASE_URL = "https://assets.blockstream.info"
payload = {
"contract": {"name": "BMN Series 2", "entity": {"domain": "blockstream-mining2.stokr.io"}, "ticker": "BMN2", "version": 0, "precision": 4, "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"},
"contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
}
res = requests.post(f"{BASE_URL}/contract/validate", json=payload, timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.text)package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
baseURL := "https://assets.blockstream.info"
endpoint := baseURL + "/contract/validate"
payload, err := json.Marshal(map[string]any{"contract": map[string]any{"name": "BMN Series 2", "entity": map[string]any{"domain": "blockstream-mining2.stokr.io"}, "ticker": "BMN2", "version": 0, "precision": 4, "issuer_pubkey": "0206eb0dd95be6ace6d83e5e865815d73bed9e553a1a829fe8c2de74189ee2289b"}, "contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"})
if err != nil {
panic(err)
}
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}Response: 200 (production · text/plain)
validError: hash mismatch: 409 (production · application/json)
{
"error": "validation_error",
"message": "contract hash mismatch, expected 998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461",
"details": {
"expected_contract_hash": "998614e8941c5cc4fd674e1d9b29e53468ced64290ec5742273659ef103cc461"
}
}Deregister (legacy)
DELETE /:asset_id SIGNED LEGACY
Deregisters an asset with a signed message. Works for any active asset, including v2-registered ones.
Parameters
| Name | In · type | Description |
|---|---|---|
asset_id required | path · hex (64) | Liquid asset ID. Upper-case hex is accepted and normalized. |
signature required | body · Base64 | Bitcoin Signed Message signature of remove <asset_id> from registry with the asset's current issuer key. |
Example request
# Sign "remove <asset_id> from registry" with the current issuer key (see Signing requests)
SIG='0Twie/nHa6rwIsj7Ma0OaTO8lSGjR2ifU99tbA85ucxdKddWbfsn9GYWEmPc61fZnDTYBLjMOmlAqcLGyJ5ouw=='
curl -X DELETE "https://assets.blockstream.info/7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f" \
-H "Content-Type: application/json" \
-d "{\"signature\":\"$SIG\"}"// Node.js 18+ (ES module: save as .mjs or set "type": "module")
import { createECDH, createHash, createPrivateKey, sign } from "node:crypto";
const BASE_URL = "https://assets.blockstream.info";
const ASSET_ID = "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f";
const PRIVATE_KEY = process.env.ISSUER_PRIVATE_KEY; // 32-byte hex: the asset's current issuer key
const signature = signMessage(`remove ${ASSET_ID} from registry`, PRIVATE_KEY);
const res = await fetch(`${BASE_URL}/${ASSET_ID}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ signature }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.text());
// --- signing helpers (see Signing requests) ---
// Canonical JSON: keys sorted recursively, no whitespace. Use integers or strings for numbers
// (non-integer numbers are formatted differently by JavaScript and the registry).
function canonicalJson(v) {
if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
if (v && typeof v === "object") return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(",")}}`;
return JSON.stringify(v);
}
// Bitcoin Signed Message signature (Base64, 64-byte R||S) over the UTF-8 message.
function signMessage(message, privateKeyHex) {
const varint = (n) => (n < 0xfd ? Buffer.from([n]) : Buffer.from([0xfd, n & 0xff, n >> 8]));
const msg = Buffer.from(message, "utf8");
const magic = Buffer.from("Bitcoin Signed Message:\n");
const firstHash = createHash("sha256").update(Buffer.concat([varint(magic.length), magic, varint(msg.length), msg])).digest();
const d = Buffer.from(privateKeyHex, "hex");
const ecdh = createECDH("secp256k1");
ecdh.setPrivateKey(d);
const pub = ecdh.getPublicKey();
const key = createPrivateKey({ format: "jwk", key: { kty: "EC", crv: "secp256k1", d: d.toString("base64url"),
x: pub.subarray(1, 33).toString("base64url"), y: pub.subarray(33).toString("base64url") } });
const sig = sign("sha256", firstHash, { key, dsaEncoding: "ieee-p1363" }); // ECDSA-SHA256 adds the second hash
const n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const s = BigInt("0x" + sig.subarray(32).toString("hex"));
if (s > n / 2n) sig.write((n - s).toString(16).padStart(64, "0"), 32, "hex"); // low-S form
return sig.toString("base64");
}
// Compressed public key (66 hex) for a private key.
function publicKeyOf(privateKeyHex) {
const ecdh = createECDH("secp256k1");
ecdh.setPrivateKey(Buffer.from(privateKeyHex, "hex"));
return ecdh.getPublicKey("hex", "compressed");
}# pip install requests cryptography==50.0.2
import base64
import hashlib
import json
import os
import requests
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
BASE_URL = "https://assets.blockstream.info"
ASSET_ID = "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f"
PRIVATE_KEY = os.environ["ISSUER_PRIVATE_KEY"] # 32-byte hex: the asset's current issuer key
def canonical_json(value):
"""Canonical JSON: keys sorted recursively, no whitespace. Prefer integers or strings for numbers."""
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def sign_message(message, private_key_hex):
"""Bitcoin Signed Message signature (Base64, 64-byte R||S) over the UTF-8 message."""
msg = message.encode("utf-8")
varint = lambda n: bytes([n]) if n < 0xFD else b"\xfd" + n.to_bytes(2, "little")
payload = varint(24) + b"Bitcoin Signed Message:\n" + varint(len(msg)) + msg
key = ec.derive_private_key(int(private_key_hex, 16), ec.SECP256K1())
first_hash = hashlib.sha256(payload).digest() # ECDSA-SHA256 adds the second hash
r, s = decode_dss_signature(key.sign(first_hash, ec.ECDSA(hashes.SHA256())))
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
s = min(s, n - s) # low-S form
return base64.b64encode(r.to_bytes(32, "big") + s.to_bytes(32, "big")).decode()
def public_key_of(private_key_hex):
"""Compressed public key (66 hex) for a private key."""
key = ec.derive_private_key(int(private_key_hex, 16), ec.SECP256K1())
return key.public_key().public_bytes(serialization.Encoding.X962, serialization.PublicFormat.CompressedPoint).hex()
signature = sign_message(f"remove {ASSET_ID} from registry", PRIVATE_KEY)
res = requests.delete(f"{BASE_URL}/{ASSET_ID}", json={"signature": signature}, timeout=30)
if not res.ok:
raise RuntimeError(f"{res.status_code} {res.text}")
print(res.text)// go mod init example && go get github.com/decred/dcrd/dcrec/secp256k1/[email protected]
package main
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)
func main() {
baseURL := "https://assets.blockstream.info"
assetID := "7ce1c2f1f3d7604eaf915c8d4f6ea03f2cbd9e7f50314c6d8eaf2041c6d8ea2f"
endpoint := baseURL + "/" + assetID
privateKey := os.Getenv("ISSUER_PRIVATE_KEY") // 32-byte hex: the asset's current issuer key
sig := signMessage([]byte("remove "+assetID+" from registry"), privateKey)
payload, err := json.Marshal(map[string]string{"signature": sig})
if err != nil {
panic(err)
}
req, err := http.NewRequest("DELETE", endpoint, bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
if res.StatusCode != http.StatusOK {
panic(fmt.Sprintf("%d %s", res.StatusCode, body))
}
fmt.Println(string(body))
}
// canonicalJSON: keys sorted (Go sorts map keys), no whitespace, no HTML escaping.
func canonicalJSON(v any) []byte {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(v); err != nil {
panic(err)
}
return bytes.TrimRight(buf.Bytes(), "\n")
}
// signMessage returns a Bitcoin Signed Message signature (Base64, 64-byte R||S).
func signMessage(message []byte, privateKeyHex string) string {
key, err := hex.DecodeString(privateKeyHex)
if err != nil {
panic(err)
}
var payload bytes.Buffer
writeVarint(&payload, 24)
payload.WriteString("Bitcoin Signed Message:\n")
writeVarint(&payload, len(message))
payload.Write(message)
first := sha256.Sum256(payload.Bytes())
digest := sha256.Sum256(first[:])
sig := ecdsa.SignCompact(secp256k1.PrivKeyFromBytes(key), digest[:], true) // recovery byte + R||S, low-S
return base64.StdEncoding.EncodeToString(sig[1:])
}
func writeVarint(b *bytes.Buffer, n int) {
if n < 0xfd {
b.WriteByte(byte(n))
return
}
b.WriteByte(0xfd)
b.WriteByte(byte(n))
b.WriteByte(byte(n >> 8))
}
// publicKeyOf returns the compressed public key (66 hex) for a private key.
func publicKeyOf(privateKeyHex string) string {
key, err := hex.DecodeString(privateKeyHex)
if err != nil {
panic(err)
}
return hex.EncodeToString(secp256k1.PrivKeyFromBytes(key).PubKey().SerializeCompressed())
}Response: 200 (local instance · text/plain)
Asset deletedUpdated about 4 hours ago