Skip to content

LLC:gist tx

@greweb edited this page Feb 14, 2023 · 2 revisions

gist: transaction with a Ledger device

We start a new project and add live-common and some helpers

yarn add @ledgerhq/live-common
yarn add react # somehow an implicit dep of live-common

Now we need a concrete implementation of a Transport to use the ledger device with. In our example we're going to do a Node.js script that works with USB, so we're just going to install these:

yarn add @ledgerhq/hw-transport-node-hid-noevents

We're all set up, let's write a script that send some bitcoin!

const { first, map, reduce, tap } = require("rxjs/operators");
const {
  getCryptoCurrencyById,
  formatCurrencyUnit,
  parseCurrencyUnit,
  setSupportedCurrencies,
} = require("@ledgerhq/live-common/lib/currencies/index");
const {
  getCurrencyBridge,
  getAccountBridge,
} = require("@ledgerhq/live-common/lib/bridge/index");

// our small example is a script that takes 3 params.
// example: node send.ts bitcoin bc1abc..def 0.001
if (!process.argv[4]) {
  console.log(`Usage: currencyId recipient amount`);
  process.exit(1);
}
const currencyId = process.argv[2]; // example "ethereum"
const currency = getCryptoCurrencyById(currencyId);
const recipient = process.argv[3]; // example "0.1234"
const amount = parseCurrencyUnit(currency.units[0], process.argv[4]);
// ^the amount is actually stored as a BigNumber of the value in "smallest unit" of the currency. (eg. in wei or in satoshi)
const deviceId = ""; // in HID case

//////////////////////////////////
// live-common requires some setup. usually we put that in a live-common-setup.js

const {
  registerTransportModule,
} = require("@ledgerhq/live-common/lib/hw/index");
const TransportNodeHid =
  require("@ledgerhq/hw-transport-node-hid-noevents").default;

// configure which coins to enable
setSupportedCurrencies([currencyId]);

// configure which transport are available
registerTransportModule({
  id: "hid",
  open: (devicePath) => TransportNodeHid.open(devicePath),
  disconnect: () => Promise.resolve(),
});

/////////////////////////

async function main() {
  ///////////////////
  // we scan the device to find an account with a balance

  // currency bridge is the interface to scan accounts of the device
  const currencyBridge = getCurrencyBridge(currency);

  // some currency requires some data to be loaded (today it's not highly used but will be more and more)
  const data = await currencyBridge.preload(currency);
  if (data) {
    currencyBridge.hydrate(currency, data);
  }

  // in our case, we don't need to paginate
  const syncConfig = { paginationConfig: {} };

  // NB scanAccountsOnDevice returns an observable but we'll just get the first account as a promise.
  const scannedAccount = await currencyBridge
    .scanAccounts({ currency, deviceId, syncConfig })
    .pipe(
      // there can be many accounts, for sake of example we take first non empty
      first((e) => e.type === "discovered" && e.account.balance.gt(0)),
      map((e) => e.account)
    )
    .toPromise();

  // account bridge is the interface to sync and do transaction on our account
  const accountBridge = getAccountBridge(scannedAccount);

  // Minimal way to synchronize an account.
  // NB: our scannedAccount is already sync in fact, this is just for the example
  const account = await accountBridge
    .sync(scannedAccount, syncConfig)
    .pipe(reduce((a, f) => f(a), scannedAccount))
    .toPromise();

  console.log(`${account.name} new address: ${account.freshAddress}`);
  console.log(
    `with balance of ${formatCurrencyUnit(account.unit, account.balance)}`
  );

  ////////////////////////////////////////////////////////////////////////
  // We prepare a transaction

  // we initialize a transaction object to work on a given account
  // this bootstraps a JSON object with the right shape
  let t = accountBridge.createTransaction(account);

  // we update the transaction with the recipient and amount
  // you can update as much field as needed and this may depends on each coin
  // usually you can see this being documented through the Transaction type of each coin
  // amount and recipient are part of the 'common' fields
  // they can be updated separately too
  t = accountBridge.updateTransaction(t, { amount, recipient });

  // each time we update the fields, we need to "prepare" the transaction
  // if we want to get any feedback that "the transaction is ready to be signed"
  // or if we need to show the UI its errors

  // prepareTransaction deal with asynchronous network calls typically necessary to updates fees information OR calculate any coin specific work.
  t = await accountBridge.prepareTransaction(account, t);

  // From a prepared transaction, we are able to get the status of the transaction
  // The status contains all derived data or meta information (like calculated fees)
  // but more importantly will contain all the warnings and errors for the UI to display
  const status = await accountBridge.getTransactionStatus(account, t);
  console.log({ status });

  // example: status.warnings.recipient for Ethereum when you don't respect EIP55 checksum
  // example: status.errors.amount if you don't have enough balance

  // The important rule being: we can't broadcast the transaction if there are errors
  const errors = Object.values(status.errors);
  if (errors.length) {
    throw errors[0];
  }

  // but if there are no error the contract of a coin is that
  // it means the transaction is possible and ready to sign and broadcast

  // We're good now, we can sign the transaction with the device
  const signedOperation = await accountBridge
    .signOperation({ account, transaction: t, deviceId })
    .pipe(
      // it is a stream of events where you can notify the UI with progress, etc..
      tap((e) => console.log(e)), // log events
      // there are many events. we just take the final signed
      first((e) => e.type === "signed"),
      map((e) => e.signedOperation)
    )
    .toPromise();

  // we now have a signed operation, and notably a signature that we can then broadcast to the network
  const operation = await accountBridge.broadcast({ account, signedOperation });

  // the transaction is broadcasted!
  // the resulting operation is an "optimistic" response that can be prepended to our account.operations[]
  console.log("broadcasted", operation);
}

main();
Clone this wiki locally