Source: custom.js

  1. /**
  2. * Provides classes for interacting via text-based copy/paste XPUBs and
  3. * download/sign generic PSBT files using a custom "device'
  4. *
  5. * The following API classes are implemented:
  6. *
  7. * * CustomExportExtendedPublicKey
  8. * * CustomSignMultisigTransaction
  9. *
  10. * @module custom
  11. */
  12. import {
  13. unsignedMultisigPSBT,
  14. parseSignaturesFromPSBT,
  15. MAINNET,
  16. TESTNET,
  17. validateBIP32Path,
  18. validateRootFingerprint,
  19. ExtendedPublicKey,
  20. } from "unchained-bitcoin";
  21. import {
  22. IndirectKeystoreInteraction,
  23. PENDING,
  24. ACTIVE,
  25. INFO,
  26. ERROR,
  27. } from "./interaction";
  28. export const CUSTOM = "custom";
  29. /**
  30. * Base class for interactions with Custom "devices"
  31. *
  32. * @extends {module:interaction.IndirectKeystoreInteraction}
  33. */
  34. export class CustomInteraction extends IndirectKeystoreInteraction {}
  35. /**
  36. * Base class for text-based (or clipboard pasted) ExtendedPublicKey
  37. * This class handles parsing/validating the xpub and relevant
  38. * derivation properties. If no root fingerprint is provided, one will
  39. * be deterministically assigned.
  40. *
  41. * @extends {module:custom.CustomInteraction}
  42. * @example
  43. * const interaction = new CustomExportExtendedPublicKey({network: MAINNET, bip32Path: "m/45'/0'/0'"});
  44. * const {xpub, rootFingerprint, bip32Path} = interaction.parse({xpub: xpub..., rootFingerprint: 0f056943});
  45. * console.log(xpub);
  46. * // "xpub..."
  47. * console.log(rootFingerprint);
  48. * // "0f056943"
  49. * console.log(bip32Path);
  50. * // "m/45'/0'/0'"
  51. * ** OR **
  52. * * const {xpub, rootFingerprint, bip32Path} = interaction.parse({xpub: xpub...});
  53. * console.log(xpub);
  54. * // "xpub..."
  55. * console.log(rootFingerprint);
  56. * // "096aed5e"
  57. * console.log(bip32Path);
  58. * // "m/45'/0'/0'"
  59. */
  60. export class CustomExportExtendedPublicKey extends CustomInteraction {
  61. /**
  62. * @param {object} options - options argument
  63. * @param {string} options.network - bitcoin network (needed for derivations)
  64. * @param {string} options.bip32Path - bip32Path to interrogate
  65. */
  66. constructor({ network, bip32Path }) {
  67. super();
  68. if ([MAINNET, TESTNET].find((net) => net === network)) {
  69. this.network = network;
  70. } else {
  71. throw new Error("Unknown network.");
  72. }
  73. this.validationErrorMessages = [];
  74. this.bip32Path = bip32Path;
  75. const bip32PathError = validateBIP32Path(bip32Path);
  76. if (bip32PathError.length) {
  77. this.validationErrorMessages.push({
  78. code: "custom.bip32_path.path_error",
  79. text: bip32PathError,
  80. });
  81. }
  82. }
  83. isSupported() {
  84. return this.validationErrorMessages.length === 0;
  85. }
  86. messages() {
  87. const messages = super.messages();
  88. if (this.validationErrorMessages.length) {
  89. this.validationErrorMessages.forEach((e) => {
  90. messages.push({
  91. state: PENDING,
  92. level: ERROR,
  93. code: e.code,
  94. text: e.text,
  95. });
  96. });
  97. }
  98. messages.push({
  99. state: PENDING,
  100. level: INFO,
  101. code: "custom.import_xpub",
  102. text: "Type or paste the extended public key here.",
  103. });
  104. return messages;
  105. }
  106. /**
  107. * Parse the provided JSON and do some basic error checking
  108. *
  109. * @param {Object} data - JSON object with incoming data to be parsed (read: reformatted)
  110. * @returns {Object} Object - ExtendedPublicKeyDerivation {xpub, bip32path, rootFingerprint}
  111. */
  112. parse(data) {
  113. // build ExtendedPublicKey struct (validation happens in constructor)
  114. let xpubClass;
  115. let rootFingerprint;
  116. try {
  117. xpubClass = ExtendedPublicKey.fromBase58(data.xpub);
  118. } catch (e) {
  119. throw new Error("Not a valid ExtendedPublicKey.");
  120. }
  121. try {
  122. if (data.rootFingerprint === "" || !data.rootFingerprint) {
  123. const pkLen = xpubClass.pubkey.length;
  124. // If no fingerprint is provided, we will assign one deterministically
  125. rootFingerprint = xpubClass.pubkey.substring(pkLen - 8);
  126. } else {
  127. validateRootFingerprint(data.rootFingerprint);
  128. rootFingerprint = data.rootFingerprint;
  129. }
  130. } catch (e) {
  131. throw new Error(
  132. `Root fingerprint validation error: ${e.message.toLowerCase()}.`
  133. );
  134. }
  135. const numSlashes = this.bip32Path.split("/").length;
  136. const bipDepth = this.bip32Path.startsWith("m/")
  137. ? numSlashes - 1
  138. : numSlashes;
  139. if (xpubClass.depth !== bipDepth) {
  140. throw new Error(
  141. `Depth of ExtendedPublicKey (${xpubClass.depth}) does not match depth of BIP32 path (${bipDepth}).`
  142. );
  143. }
  144. return {
  145. xpub: xpubClass.base58String,
  146. rootFingerprint,
  147. bip32Path: this.bip32Path,
  148. };
  149. }
  150. }
  151. /**
  152. * Returns signature request data via a PSBT for a Custom "device" to sign and
  153. * accepts a PSBT for parsing signatures from a Custom "device"
  154. *
  155. * @extends {module:custom.CustomInteraction}
  156. * @example
  157. * const interaction = new CustomSignMultisigTransaction({network, inputs, outputs, bip32paths, psbt});
  158. * console.log(interaction.request());
  159. * // "cHNidP8BA..."
  160. *
  161. * // Parse signatures from a signed PSBT
  162. * const signatures = interaction.parse(psbt);
  163. * console.log(signatures);
  164. * // {'029e866...': ['3045...01', ...]}
  165. *
  166. */
  167. export class CustomSignMultisigTransaction extends CustomInteraction {
  168. /**
  169. * @param {object} options - options argument
  170. * @param {string} options.network - bitcoin network
  171. * @param {array<object>} options.inputs - inputs for the transaction
  172. * @param {array<object>} options.outputs - outputs for the transaction
  173. * @param {array<string>} options.bip32Paths - BIP32 paths
  174. * @param {object} [options.psbt] - PSBT of the transaction to sign, included or generated from the other options
  175. */
  176. constructor({ network, inputs, outputs, bip32Paths, psbt }) {
  177. super();
  178. this.network = network;
  179. this.inputs = inputs;
  180. this.outputs = outputs;
  181. this.bip32Paths = bip32Paths;
  182. if (psbt) {
  183. this.psbt = psbt;
  184. } else {
  185. try {
  186. this.psbt = unsignedMultisigPSBT(network, inputs, outputs);
  187. } catch (e) {
  188. throw new Error(
  189. "Unable to build the PSBT from the provided parameters."
  190. );
  191. }
  192. }
  193. }
  194. messages() {
  195. const messages = super.messages();
  196. messages.push({
  197. state: PENDING,
  198. level: INFO,
  199. code: "custom.download_psbt",
  200. text: `Download and save this PSBT file.`,
  201. });
  202. messages.push({
  203. state: PENDING,
  204. level: INFO,
  205. code: "custom.sign_psbt",
  206. text: `Add your signature to the PSBT.`,
  207. });
  208. messages.push({
  209. state: ACTIVE,
  210. level: INFO,
  211. code: "custom.sign_psbt",
  212. text: `Verify the transaction details and sign.`,
  213. });
  214. messages.push({
  215. state: ACTIVE,
  216. level: INFO,
  217. code: "custom.upload_signed_psbt",
  218. text: `Upload the signed PSBT.`,
  219. });
  220. return messages;
  221. }
  222. /**
  223. * Request for the PSBT data that needs to be signed.
  224. *
  225. * NOTE: the application may be expecting the PSBT in some format
  226. * other than the direct Object.
  227. *
  228. * E.g. PSBT in Base64 is interaction().request().toBase64()
  229. *
  230. * @returns {Object} Returns the local unsigned PSBT from transaction details
  231. */
  232. request() {
  233. return this.psbt;
  234. }
  235. /**
  236. *
  237. * @param {Object} psbtObject - the PSBT
  238. * @returns {Object} signatures - This calls a function in unchained-bitcoin which parses
  239. * PSBT files for sigantures and then returns an object with the format
  240. * {
  241. * pubkey1 : [sig1, sig2, ...],
  242. * pubkey2 : [sig1, sig2, ...]
  243. * }
  244. * This format may change in the future or there may be additional options for return type.
  245. */
  246. parse(psbtObject) {
  247. const signatures = parseSignaturesFromPSBT(psbtObject);
  248. if (!signatures || signatures.length === 0) {
  249. throw new Error(
  250. "No signatures found in the PSBT. Did you upload the right one?"
  251. );
  252. }
  253. return signatures;
  254. }
  255. }