|
| 1 | +import Fs from "node:fs/promises"; |
| 2 | + |
| 3 | +// import DashKeys from "dashkeys"; |
| 4 | +import * as DashTx from "dashtx/dashtx.js"; |
| 5 | + |
| 6 | +import * as Bincode from "./bincode.ts"; |
| 7 | +import * as DashBincode from "./1.8.1/generated_bincode.js"; |
| 8 | +import * as KeyUtils from "./key-utils.js"; |
| 9 | +import baseX from "base-x"; |
| 10 | + |
| 11 | +const ST_CREATE_IDENTITY = 2; |
| 12 | +const L2_VERSION_PLATFORM = 1; // actually constant "0" ?? |
| 13 | + |
| 14 | +let KEY_LEVELS = { |
| 15 | + 0: "MASTER", |
| 16 | + 1: "CRITICAL", |
| 17 | + 2: "HIGH", |
| 18 | + 3: "MEDIUM", |
| 19 | + MASTER: 0, |
| 20 | + CRITICAL: 1, |
| 21 | + HIGH: 2, |
| 22 | + MEDIUM: 3, |
| 23 | +}; |
| 24 | + |
| 25 | +let KEY_PURPOSES = { |
| 26 | + 0: "AUTHENTICATION", |
| 27 | + 1: "ENCRYPTION", |
| 28 | + 2: "DECRYPTION", |
| 29 | + 3: "TRANSFER", |
| 30 | + 4: "SYSTEM", |
| 31 | + 5: "VOTING", |
| 32 | + AUTHENTICATION: 0, |
| 33 | + ENCRYPTION: 1, |
| 34 | + DECRYPTION: 2, |
| 35 | + TRANSFER: 3, |
| 36 | + SYSTEM: 4, |
| 37 | + VOTING: 5, |
| 38 | +}; |
| 39 | + |
| 40 | +let KEY_TYPES = { |
| 41 | + 0: "ECDSA_SECP256K1", |
| 42 | + ECDSA_SECP256K1: 0, |
| 43 | +}; |
| 44 | + |
| 45 | +const BASE58 = `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`; |
| 46 | +let base58 = baseX(BASE58); |
| 47 | + |
| 48 | +let Thingy = {}; |
| 49 | + |
| 50 | +/** |
| 51 | + * @typedef AssetLockChainProof |
| 52 | + * @prop {Number} core_chain_locked_height |
| 53 | + * @prop {Object} out_point |
| 54 | + * @prop {String} out_point.txid |
| 55 | + * @prop {Number} out_point.vout |
| 56 | + */ |
| 57 | + |
| 58 | +/** |
| 59 | + * @param {import('dashhd').HDWallet} assetKey |
| 60 | + * @param {import('dashhd').HDWallet} masterKey |
| 61 | + * @param {import('dashhd').HDWallet} otherKey |
| 62 | + * @param {String} identityIdHex |
| 63 | + * @param {String} txidHex |
| 64 | + * @param {String} [txlocksigHex] |
| 65 | + * @param {import('dashtx').TxInfo} [txCore] |
| 66 | + */ |
| 67 | +Thingy.doStuff = async function ( |
| 68 | + assetKey, |
| 69 | + masterKey, |
| 70 | + otherKey, |
| 71 | + identityIdHex, |
| 72 | + txidHex, |
| 73 | + txlocksigHex, |
| 74 | + txCore, |
| 75 | +) { |
| 76 | + // const INSTANT_ALP = 0; |
| 77 | + // const CHAIN_ALP = 1; |
| 78 | + |
| 79 | + /** @type {DashBincode.AssetLockProof} */ |
| 80 | + let assetLockProof; |
| 81 | + if (txlocksigHex) { |
| 82 | + assetLockProof = await getAssetLockInstantProof(txlocksigHex); |
| 83 | + } else { |
| 84 | + assetLockProof = await getAssetLockChainProof(txidHex, txCore); |
| 85 | + } |
| 86 | + |
| 87 | + if (!masterKey.privateKey) { |
| 88 | + throw new Error("'masterKey' is missing 'privateKey'"); |
| 89 | + } |
| 90 | + if (!otherKey.privateKey) { |
| 91 | + throw new Error("'otherKey' is missing 'privateKey'"); |
| 92 | + } |
| 93 | + let identityKeys = await getKnownIdentityKeys( |
| 94 | + { privateKey: masterKey.privateKey, publicKey: masterKey.publicKey }, |
| 95 | + { privateKey: otherKey.privateKey, publicKey: otherKey.publicKey }, |
| 96 | + ); |
| 97 | + let stKeys = getIdentityTransitionKeys(identityKeys); |
| 98 | + |
| 99 | + let identityCreate = DashBincode.IdentityCreateTransitionV0({ |
| 100 | + //protocolVersion: L2_VERSION_PLATFORM, |
| 101 | + // $version: L2_VERSION_PLATFORM.toString(), |
| 102 | + // type: ST_CREATE_IDENTITY, |
| 103 | + // ecdsaSig(assetLockPrivateKey, CBOR(thisStateTransition)) |
| 104 | + // "signature":"IBTTgge+/VDa/9+n2q3pb4tAqZYI48AX8X3H/uedRLH5dN8Ekh/sxRRQQS9LaOPwZSCVED6XIYD+vravF2dhYOE=", |
| 105 | + asset_lock_proof: assetLockProof, |
| 106 | + // publicKeys: stKeys, |
| 107 | + public_keys: stKeys, |
| 108 | + // [ |
| 109 | + // { |
| 110 | + // id: 0, |
| 111 | + // type: 0, |
| 112 | + // purpose: 0, |
| 113 | + // securityLevel: 0, |
| 114 | + // data: "AkWRfl3DJiyyy6YPUDQnNx5KERRnR8CoTiFUvfdaYSDS", |
| 115 | + // readOnly: false, |
| 116 | + // }, |
| 117 | + // ], |
| 118 | + identity_id: DashBincode.Identifier(DashBincode.IdentifierBytes32(DashTx.utils.hexToBytes(identityIdHex))), |
| 119 | + user_fee_increase: 0, |
| 120 | + signature: DashBincode.BinaryData(new Uint8Array), |
| 121 | + }) |
| 122 | + |
| 123 | + let stateTransition = DashBincode.StateTransition.IdentityCreate( |
| 124 | + DashBincode.IdentityCreateTransition.V0(identityCreate)); |
| 125 | + console.log(`stKeys:`); |
| 126 | + console.log(stKeys); |
| 127 | + |
| 128 | + let nullSigTransition = new Uint8Array(Bincode.encode( |
| 129 | + DashBincode.StateTransition, |
| 130 | + stateTransition, |
| 131 | + { |
| 132 | + signable: true, |
| 133 | + }, |
| 134 | + )); |
| 135 | + console.log(); |
| 136 | + console.log(`nullSigTransition (ready-to-sign by identity keys):`); |
| 137 | + console.log('(hex)', DashTx.utils.bytesToHex(nullSigTransition)); |
| 138 | + console.log('(base64)', bytesToBase64(nullSigTransition)); |
| 139 | + |
| 140 | + let nullSigMagicHash = await KeyUtils.doubleSha256(nullSigTransition); |
| 141 | + |
| 142 | + if (!assetKey.privateKey) { |
| 143 | + throw new Error("'assetKey' is missing 'privateKey'"); |
| 144 | + } |
| 145 | + { |
| 146 | + let magicSigBytes = await KeyUtils.magicSign({ |
| 147 | + privKeyBytes: assetKey.privateKey, |
| 148 | + doubleSha256Bytes: nullSigMagicHash, |
| 149 | + }); |
| 150 | + |
| 151 | + identityCreate.signature[0] = magicSigBytes |
| 152 | + } |
| 153 | + |
| 154 | + for (let i = 0; i < identityKeys.length; i += 1) { |
| 155 | + let key = identityKeys[i]; |
| 156 | + let stPub = identityCreate.public_keys[i]; |
| 157 | + let magicSigBytes = await KeyUtils.magicSign({ |
| 158 | + privKeyBytes: key.privateKey, |
| 159 | + doubleSha256Bytes: nullSigMagicHash, |
| 160 | + }); |
| 161 | + |
| 162 | + Bincode.match(stPub, { |
| 163 | + V0: ({0: stPub0}) => { |
| 164 | + stPub0.signature[0] = magicSigBytes |
| 165 | + } |
| 166 | + }) |
| 167 | + } |
| 168 | + |
| 169 | + console.log(); |
| 170 | + console.log(JSON.stringify(stateTransition, (key, val) => { |
| 171 | + if (val instanceof Uint8Array || val instanceof ArrayBuffer) { |
| 172 | + return {'@Uint8Array hex': DashTx.utils.bytesToHex(new Uint8Array(val))} |
| 173 | + } |
| 174 | + return val |
| 175 | + }, 2)); |
| 176 | + |
| 177 | + let grpcTransition = ""; |
| 178 | + let transitionHashHex = ""; |
| 179 | + { |
| 180 | + let fullSigTransition = new Uint8Array(Bincode.encode( |
| 181 | + DashBincode.StateTransition, |
| 182 | + stateTransition, |
| 183 | + { |
| 184 | + signable: false, |
| 185 | + }, |
| 186 | + )); |
| 187 | + console.log(); |
| 188 | + console.log(`transition (fully signed):`); |
| 189 | + console.log(DashTx.utils.bytesToHex(fullSigTransition)); |
| 190 | + let transitionHash = await KeyUtils.sha256(fullSigTransition); |
| 191 | + transitionHashHex = DashTx.utils.bytesToHex(transitionHash); |
| 192 | + grpcTransition = bytesToBase64(fullSigTransition); |
| 193 | + } |
| 194 | + |
| 195 | + console.log(); |
| 196 | + console.log(); |
| 197 | + console.log(`grpcurl -plaintext -d '{ |
| 198 | + "stateTransition": "${grpcTransition}" |
| 199 | +}' seed-2.testnet.networks.dash.org:1443 org.dash.platform.dapi.v0.Platform.broadcastStateTransition`); |
| 200 | + console.log(); |
| 201 | + let identityIdBytes = DashTx.utils.hexToBytes(identityIdHex); |
| 202 | + let identity = base58.encode(identityIdBytes); |
| 203 | + console.log(`https://testnet.platform-explorer.com/identity/${identity}`); |
| 204 | + console.log( |
| 205 | + `https://testnet.platform-explorer.com/transaction/${transitionHashHex}`, |
| 206 | + ); |
| 207 | +}; |
| 208 | + |
| 209 | +export default Thingy; |
| 210 | + |
| 211 | +/** @param {HexString} txlocksigHex */ |
| 212 | +async function getAssetLockInstantProof(txlocksigHex) { |
| 213 | + { |
| 214 | + let len = txlocksigHex.length / 2; |
| 215 | + console.log(); |
| 216 | + console.log(`Tx Lock Sig Hex (${len}):`); |
| 217 | + console.log(txlocksigHex); |
| 218 | + } |
| 219 | + |
| 220 | + let vout = -1; |
| 221 | + let instantLockTxHex = ""; |
| 222 | + let instantLockSigHex = ""; |
| 223 | + { |
| 224 | + let txlocksig = DashTx.parseUnknown(txlocksigHex); |
| 225 | + vout = 0; |
| 226 | + //vout = txlocksig.extraPayload.outputs.findIndex(function (output) { |
| 227 | + // //@ts-expect-error |
| 228 | + // return output.script === "6a00"; |
| 229 | + //}); |
| 230 | + // console.log(txlocksig.extraPayload.outputs); |
| 231 | + //@ts-expect-error |
| 232 | + instantLockSigHex = txlocksig.sigHashTypeHex; |
| 233 | + let isLen = instantLockSigHex.length / 2; |
| 234 | + let len = txlocksigHex.length / 2; |
| 235 | + len -= isLen; |
| 236 | + instantLockTxHex = txlocksigHex.slice(0, len * 2); |
| 237 | + console.log(); |
| 238 | + console.log(`Tx Hex (${len})`); |
| 239 | + console.log(instantLockTxHex); |
| 240 | + console.log(); |
| 241 | + console.log(`Tx Lock Sig Instant Lock Hex (${isLen})`); |
| 242 | + //@ts-expect-error |
| 243 | + console.log(txlocksig.sigHashTypeHex); |
| 244 | + } |
| 245 | + |
| 246 | + let assetLockInstantProof = DashBincode.RawInstantLockProof({ |
| 247 | + instant_lock: DashBincode.BinaryData(DashTx.utils.hexToBytes(instantLockSigHex)), |
| 248 | + transaction: DashBincode.BinaryData(DashTx.utils.hexToBytes(instantLockTxHex)), // TODO this may need the proof, not the signed tx |
| 249 | + output_index: vout, |
| 250 | + }); |
| 251 | + return DashBincode.AssetLockProof.Instant(assetLockInstantProof); |
| 252 | +} |
| 253 | + |
| 254 | +/** |
| 255 | + * @param {HexString} txidHex |
| 256 | + * @param {any} txInfo - TODO CoreTx |
| 257 | + */ |
| 258 | +async function getAssetLockChainProof(txidHex, txInfo) { |
| 259 | + //@ts-expect-error |
| 260 | + let vout = txInfo.vout.findIndex(voutInfo => |
| 261 | + voutInfo.scriptPubKey?.hex === "6a00" // TODO match the burn |
| 262 | + ); |
| 263 | + |
| 264 | + let assetLockChainProof = DashBincode.ChainAssetLockProof({ |
| 265 | + core_chain_locked_height: txInfo.height, |
| 266 | + out_point: { |
| 267 | + txid: DashBincode.Txid(DashTx.utils.hexToBytes(txidHex)), |
| 268 | + vout: vout, |
| 269 | + }, |
| 270 | + }); |
| 271 | + |
| 272 | + return DashBincode.AssetLockProof.Chain(assetLockChainProof); |
| 273 | +} |
| 274 | + |
| 275 | +/** |
| 276 | + * @param {Required<Pick<import('dashhd').HDXKey, "privateKey"|"publicKey">>} masterKey |
| 277 | + * @param {Required<Pick<import('dashhd').HDXKey, "privateKey"|"publicKey">>} otherKey |
| 278 | + * @returns {Promise<Array<EvoKey>>} |
| 279 | + */ |
| 280 | +async function getKnownIdentityKeys(masterKey, otherKey) { |
| 281 | + if (!masterKey.privateKey) { |
| 282 | + throw new Error("linter fail"); |
| 283 | + } |
| 284 | + if (!otherKey.privateKey) { |
| 285 | + throw new Error("linter fail"); |
| 286 | + } |
| 287 | + let keyDescs = [ |
| 288 | + // {"$version":"0","id":0,"purpose":0,"securityLevel":0,"contractBounds":null,"type":0,"readOnly":false,"data":[3,58,154,139,30,76,88,26,25,135,114,76,102,151,19,93,49,192,126,231,172,130,126,106,89,206,192,34,176,77,81,5,95],"disabledAt":null} |
| 289 | + { |
| 290 | + id: 0, |
| 291 | + type: DashBincode.KeyType.ECDSA_SECP256K1(), |
| 292 | + purpose: DashBincode.Purpose.AUTHENTICATION(), |
| 293 | + securityLevel: DashBincode.SecurityLevel.MASTER(), |
| 294 | + readOnly: false, |
| 295 | + publicKey: masterKey.publicKey, |
| 296 | + privateKey: masterKey.privateKey, |
| 297 | + data: "", |
| 298 | + }, |
| 299 | + // {"$version":"0","id":1,"purpose":0,"securityLevel":1,"contractBounds":null,"type":0,"readOnly":false,"data":[2,1,70,3,1,141,196,55,100,45,218,22,244,199,252,80,228,130,221,35,226,70,128,188,179,165,150,108,59,52,56,72,226],"disabledAt":null} |
| 300 | + { |
| 301 | + id: 1, |
| 302 | + type: DashBincode.KeyType.ECDSA_SECP256K1(), |
| 303 | + purpose: DashBincode.Purpose.AUTHENTICATION(), |
| 304 | + securityLevel: DashBincode.SecurityLevel.CRITICAL(), |
| 305 | + readOnly: false, |
| 306 | + privateKey: otherKey.privateKey, |
| 307 | + publicKey: otherKey.publicKey, |
| 308 | + data: "", |
| 309 | + }, |
| 310 | + ]; |
| 311 | + return keyDescs; |
| 312 | +} |
| 313 | + |
| 314 | +/** |
| 315 | + * @typedef EvoKey |
| 316 | + * @prop {Uint8} id |
| 317 | + * @prop {DashBincode.KeyType} type - TODO constrain to members of KEY_TYPES |
| 318 | + * @prop {DashBincode.Purpose} purpose - TODO constrain to members of KEY_PURPOSES |
| 319 | + * @prop {DashBincode.SecurityLevel} securityLevel - TODO constrain to members of KEY_LEVELS |
| 320 | + * @prop {Boolean} readOnly |
| 321 | + * @prop {Uint8Array} publicKey |
| 322 | + * @prop {Uint8Array} privateKey |
| 323 | + */ |
| 324 | + |
| 325 | +/** |
| 326 | + * @typedef STKey |
| 327 | + * @prop {Uint8} id |
| 328 | + * @prop {DashBincode.KeyType} type - TODO constrain to members of KEY_TYPES |
| 329 | + * @prop {DashBincode.Purpose} purpose - TODO constrain to members of KEY_PURPOSES |
| 330 | + * @prop {Base64} data - base64-encoded publicKey (compact) |
| 331 | + * @prop {DashBincode.SecurityLevel} securityLevel - TODO constrain to members of KEY_LEVELS |
| 332 | + * @prop {Boolean} readOnly |
| 333 | + */ |
| 334 | + |
| 335 | +/** |
| 336 | + * @param {Array<EvoKey>} identityKeys - TODO |
| 337 | + * @returns {Array<DashBincode.IdentityPublicKeyInCreation>} |
| 338 | + */ |
| 339 | +function getIdentityTransitionKeys(identityKeys) { |
| 340 | + let stKeys = []; |
| 341 | + for (let key of identityKeys) { |
| 342 | + let stKey = DashBincode.IdentityPublicKeyInCreation.V0(DashBincode.IdentityPublicKeyInCreationV0({ |
| 343 | + id: key.id, |
| 344 | + key_type: key.type, |
| 345 | + purpose: key.purpose, |
| 346 | + security_level: key.securityLevel, |
| 347 | + contract_bounds: undefined, |
| 348 | + read_only: key.readOnly || false, |
| 349 | + data: DashBincode.BinaryData(key.publicKey), |
| 350 | + signature: DashBincode.BinaryData(new Uint8Array), |
| 351 | + })); |
| 352 | + stKeys.push(stKey); |
| 353 | + } |
| 354 | + return stKeys; |
| 355 | +} |
| 356 | + |
| 357 | +/** |
| 358 | + * @param {Uint8Array} bytes |
| 359 | + */ |
| 360 | +function bytesToBase64(bytes) { |
| 361 | + // @ts-expect-error Uint8Array is close enough to number[] for this to work |
| 362 | + return btoa(String.fromCharCode.apply(null, bytes)); |
| 363 | +} |
| 364 | + |
| 365 | +/** |
| 366 | + * Reads a hex file as text, stripping comments (anything including and after a non-hex character), removing whitespace, and joining as a single string |
| 367 | + * @param {String} path |
| 368 | + */ |
| 369 | +async function readHex(path) { |
| 370 | + let text = await Fs.readFile(path, "utf8"); |
| 371 | + let lines = text.split("\n"); |
| 372 | + let hexes = []; |
| 373 | + for (let line of lines) { |
| 374 | + line = line.replace(/\s/g, ""); |
| 375 | + line = line.replace(/[^0-9a-f].*/i, ""); |
| 376 | + hexes.push(line); |
| 377 | + } |
| 378 | + |
| 379 | + let hex = hexes.join(""); |
| 380 | + return hex; |
| 381 | +} |
| 382 | + |
| 383 | +/** |
| 384 | + * @param {String} path |
| 385 | + */ |
| 386 | +async function readWif(path) { |
| 387 | + let wif = await Fs.readFile(path, "utf8"); |
| 388 | + wif = wif.trim(); |
| 389 | + |
| 390 | + return wif; |
| 391 | +} |
| 392 | + |
| 393 | +/** @typedef {String} Base58 */ |
| 394 | +/** @typedef {String} Base64 */ |
| 395 | +/** @typedef {String} HexString */ |
| 396 | +/** @typedef {Number} Uint53 */ |
| 397 | +/** @typedef {Number} Uint32 */ |
| 398 | +/** @typedef {Number} Uint8 */ |
0 commit comments