Repository navigation
Expand file tree
/
Copy pathauthUpdate.ts
More file actions
155 lines (144 loc) · 7.28 KB
/
Copy pathauthUpdate.ts
File metadata and controls
155 lines (144 loc) · 7.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { Wallet, utf8ToBin, sha256, OpReturnData, TokenSendRequest, TestNetWallet, binToHex, toBch, type Utxo } from "mainnet-js";
import { queryAuthHead } from "./queryChainGraph.js";
// import { readFileSync } from "fs";
// const bcmrJsonFile = readFileSync("bitcoin-cash-metadata-registry.json", "utf8");
// let bcmrJsonString = bcmrJsonFile;
// note: when disabling 'fetchJsonFromUrl' uncomment the 3 lines above and comment out the line below
let bcmrJsonString: undefined | string
// Fill in these config variables, or set them in the environment (TOKEN_ID, NETWORK, BCMR_URL,
// BCMR_IPFS_CID, WIF, SEEDPHRASE), which keeps keys out of this file. `--dry-run` (or DRY_RUN=true)
// stops before broadcasting.
const tokenId = process.env.TOKEN_ID || "";
// general config
const network = (process.env.NETWORK || "mainnet") as "mainnet" | "chipnet"; // mainnet or chipnet
const fetchJsonFromUrl = true; // fetch the BCMR from https or IPFS
const keepReservedSupply = false; // keeps fungible tokens on AuthHead
// bcmrURL or bcmrIpfsCID
// note: when using 'fetchJsonFromUrl' this will be fetched and used as bcmrJsonString
// a trailing slash would make the location a path instead of the domain's .well-known registry
const bcmrURL = (process.env.BCMR_URL || "").replace(/\/+$/, ""); // https link
const bcmrIpfsCID: string = process.env.BCMR_IPFS_CID || "" // IPFS CID (baf...)
// wif or seedphase + derivationPathAddress
const wif = process.env.WIF || "";
const seedphase = process.env.SEEDPHRASE || "";
const derivationPathAddress = "m/44'/145'/0'/0/0"; // last number is the address index from electron cash
// start of the program code
const dryRunEnv = (process.env.DRY_RUN || "").toLowerCase();
if(dryRunEnv && !["true", "1", "false", "0"].includes(dryRunEnv)) throw new Error("DRY_RUN should be true or false");
const dryRun = dryRunEnv == "true" || dryRunEnv == "1" || process.argv.includes("--dry-run");
if(network != "mainnet" && network != "chipnet") throw new Error("network should be mainnet or chipnet");
const ipfsGateway = "https://w3s.link/ipfs/"
const blockexplorer = "https://explorer.electroncash.de/tx/"
const authHeadTxId = await queryAuthHead(tokenId, network);
const walletClass = network == "mainnet" ? Wallet : TestNetWallet;
if(!wif && !seedphase) throw new Error("provide either a wif or a seedphrase + derivationPathAddress");
let wallet: Wallet | undefined;
if(wif) {
wallet = await walletClass.fromWIF(wif);
} else {
// mainnet-js uses m/44'/0'/0'/0/0 by default so have to overwrite it
wallet = await walletClass.fromSeed(seedphase, derivationPathAddress);
}
const walletAddress = wallet.getDepositAddress();
const balance = await wallet.getBalance();
console.log(`wallet address: ${walletAddress}`);
console.log(`Bch amount in walletAddress is ${toBch(balance)}bch or ${balance}sats`);
if(balance < 1000n) throw new Error("Not enough BCH to make the transaction!");
let authUtxo;
const utxosWallet = await wallet.getUtxos();
utxosWallet.forEach(utxo => {
if(utxo.txid == authHeadTxId && utxo.vout == 0) authUtxo = utxo;
})
console.log(`The authHead is the first output of the transaction with id ${authHeadTxId}`);
if(!bcmrURL && !bcmrIpfsCID) throw new Error("provide the BCMR location on https or IPFS");
if(bcmrURL && bcmrIpfsCID) throw new Error("provide either a https or an IPFS location for the BCMR!");
if(bcmrIpfsCID && !bcmrIpfsCID.startsWith("baf")) throw new Error("the IPFS CID shold start with baf...");
if(authUtxo) {
console.log(authUtxo)
await updateMetadata(authUtxo, bcmrURL, bcmrIpfsCID);
} else {
throw new Error("wallet does not hold the authority to update the metadata")
}
// the wallet's Electrum connection would otherwise keep the process running
process.exit();
// Function sending the onchain metadata update transaction
async function updateMetadata(
authUtxo: Utxo, bcmrURL: string, bcmrIpfsCID: string
) {
if(!wallet) throw new Error("Error creating wallet from wif or seedphrase");
// Fetch the BCMR from https or IPFS if configured so
if(fetchJsonFromUrl){
let fetchLocation = bcmrURL? bcmrURL : bcmrIpfsCID;
if(bcmrIpfsCID) fetchLocation = ipfsGateway + fetchLocation;
if(bcmrURL && !bcmrURL.startsWith("https://")) fetchLocation = "https://"+fetchLocation;
if(bcmrURL && !bcmrURL.replace(/^https:\/\//, "").includes("/")) fetchLocation += "/.well-known/bitcoin-cash-metadata-registry.json";
try {
console.log("fetching the BCMR from "+fetchLocation)
const response = await fetch(fetchLocation);
if(!response.ok) throw new Error(`status ${response.status}`);
bcmrJsonString = await response.text();
} catch (error) {
throw new Error("Error fetching the BCMR from "+fetchLocation+": "+error);
}
}
if(!bcmrJsonString) throw new Error("No bcmrJsonString available");
// refuse to commit to a file that is not a registry describing this token, such as an error page
let bcmrJson;
try {
bcmrJson = JSON.parse(bcmrJsonString);
} catch (error) {
throw new Error("The BCMR is not valid JSON");
}
if(!bcmrJson?.identities?.[tokenId]) throw new Error(`The BCMR has no identity for ${tokenId}`);
try {
// Construct opreturn output
const hashContent = sha256.hash(utf8ToBin(bcmrJsonString));
console.log("content hash: " + binToHex(hashContent))
let onchainLocation = bcmrURL? bcmrURL : bcmrIpfsCID;
if(bcmrIpfsCID) onchainLocation = "ipfs://"+onchainLocation;
if(onchainLocation.startsWith("https://")) onchainLocation =onchainLocation.slice(8);
console.log("onchain location: " + onchainLocation)
const chunks = ["BCMR", hashContent, onchainLocation];
let opreturnData = OpReturnData.fromArray(chunks);
// Construct new AuthHead output
let newAuthHead;
const bchOnlyOutput = {cashaddr: walletAddress, value: 600n}
const reservedSupplyOutput = new TokenSendRequest({
cashaddr: walletAddress,
value: 1000n,
category: tokenId,
amount: authUtxo?.token?.amount
});
newAuthHead = keepReservedSupply ? reservedSupplyOutput : bchOnlyOutput;
const outputs = [ newAuthHead, opreturnData ];
// Adding tokenChangeOutput prevents accidental token burning if authhead utxo holds tokens
let tokenChangeOutput;
if(authUtxo.token && !keepReservedSupply){
tokenChangeOutput = authUtxo.token.amount? new TokenSendRequest({
cashaddr: walletAddress,
category: tokenId,
amount: authUtxo.token.amount
}) : new TokenSendRequest({
cashaddr: walletAddress,
category: tokenId,
// NFT property can not be undefined here, because token must be either fungible with amount or have nft property
nft: {
commitment: authUtxo.token.nft!.commitment,
capability: authUtxo.token.nft!.capability
}
});
outputs.push(tokenChangeOutput)
}
if(dryRun) {
console.log("DRY_RUN: not broadcasting, the transaction would send the authhead back to " + walletAddress);
return;
}
const { txId } = await wallet.send(outputs, { ensureUtxos: [authUtxo] });
const displayId = `${authHeadTxId.slice(0, 20)}...${authHeadTxId.slice(-10)}`;
const txLink = network == "mainnet" ? blockexplorer + txId : txId;
console.log(`Published Auth update in tx ${displayId}, returned Auth to ${walletAddress} \n${txLink}`);
} catch (error) {
console.log(error);
process.exitCode = 1;
}
}