WEB3DEV

Cover image for Execute uma operação de Envio na Ledger
Diogo Jorge
Diogo Jorge

Posted on

Execute uma operação de Envio na Ledger

Este artigo foi escrito por Ledger Developer Portal e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui:

6 - Envio

Tempo estimado de leitura: Mais de 10 minutos

Começando com uma simulação

Uma simulação ajudará você a testar diferentes fluxos de interface do usuário no desktop e no celular. Ele está conectado a qualquer indexador/explorador e fornece uma ideia aproximada de como ele ficará quando conectado à interface do usuário.

Por exemplo, você pode usá-lo fazendo MOCK=1 pnpm dev:lld na ledger-live-desktop

import { BigNumber } from "bignumber.js";
import {
  NotEnoughBalance,
  RecipientRequired,
  InvalidAddress,
  FeeTooHigh,
} from "@ledgerhq/errors";
import type { Transaction } from "../types";
import type { AccountBridge, CurrencyBridge } from "../../../types";
import {
  scanAccounts,
  signOperation,
  broadcast,
  sync,
  isInvalidRecipient,
} from "../../../bridge/mockHelpers";
import { getMainAccount } from "../../../account";
import { makeAccountBridgeReceive } from "../../../bridge/mockHelpers";

const receive = makeAccountBridgeReceive();

const createTransaction = (): Transaction => ({
  family: "mycoin",
  mode: "send",
  amount: BigNumber(0),
  recipient: "",
  useAllAmount: false,
  fees: null,
});

const updateTransaction = (t, patch) => ({ ...t, ...patch });

const prepareTransaction = async (a, t) => t;

const estimateMaxSpendable = ({ account, parentAccount, transaction }) => {
  const mainAccount = getMainAccount(account, parentAccount);
  const estimatedFees = transaction?.fees || BigNumber(5000);
  return Promise.resolve(
    BigNumber.max(0, mainAccount.balance.minus(estimatedFees))
  );
};

const getTransactionStatus = (account, t) => {
  const errors = {};
  const warnings = {};
  const useAllAmount = !!t.useAllAmount;

  const estimatedFees = BigNumber(5000);

  const totalSpent = useAllAmount
    ? account.balance
    : BigNumber(t.amount).plus(estimatedFees);

  const amount = useAllAmount
    ? account.balance.minus(estimatedFees)
    : BigNumber(t.amount);

  if (amount.gt(0) && estimatedFees.times(10).gt(amount)) {
    warnings.amount = new FeeTooHigh();
  }

  if (totalSpent.gt(account.balance)) {
    errors.amount = new NotEnoughBalance();
  }

  if (!t.recipient) {
    errors.recipient = new RecipientRequired();
  } else if (isInvalidRecipient(t.recipient)) {
    errors.recipient = new InvalidAddress();
  }

  return Promise.resolve({
    errors,
    warnings,
    estimatedFees,
    amount,
    totalSpent,
  });
};

const accountBridge: AccountBridge<Transaction> = {
  estimateMaxSpendable,
  createTransaction,
  updateTransaction,
  getTransactionStatus,
  prepareTransaction,
  sync,
  receive,
  signOperation,
  broadcast,
};

const currencyBridge: CurrencyBridge = {
  scanAccounts,
  preload: async () => {},
  hydrate: () => {},
};

export default { currencyBridge, accountBridge };
Enter fullscreen mode Exit fullscreen mode

Account Bridge

AccountBridge oferece uma abstração genérica para sincronizar contas e realizar transações.

Ela é projetada para a interface front-end do usuário final e é agnóstica da maneira como é executada, possui várias implementações e não se sabe como os dados são armazenados: na verdade , é apenas um conjunto de funções sem estado.

Image description

Transações

Objetos de transação são criados a partir de um estado padrão (createTransaction), que será atualizado de acordo com o fluxo e as entradas do usuário.

Toda vez que a transação é atualizada por meio de um patch (updateTransaction), seus parâmetros precisarão ser validados para verificar se a transação pode ser assinada e transmitida (veja Validando Transações).

Em alguns casos, essa transação precisará ser preparada para verificar corretamente o status (prepareTransaction), como buscar as taxas de rede, transformar alguns parâmetros ou definir valores padrão, …

libs/ledger-live-common/src/families/mycoin/js -transaction.ts:



import { BigNumber } from "bignumber.js";
import type { Account } from "../../types";
import type { Transaction } from "./types";
import getEstimatedFees from "./js-getFeesForTransaction";
const sameFees = (a, b) => (!a || !b ? a === b : a.eq(b));
/**
 * Create an empty transaction
 *
 * @returns {Transaction}
 */
export const createTransaction = (): Transaction => ({
  family: "mycoin",
  mode: "send",
  amount: BigNumber(0),
  recipient: "",
  useAllAmount: false,
  fees: null,
});

/**
 * Apply patch to transaction
 *
 * @param {*} t
 * @param {*} patch
 */
export const updateTransaction = (
  t: Transaction,
  patch: $Shape<Transaction>
) => ({ ...t, ...patch });

/**
 * Prepare transaction before checking status
 *
 * @param {Account} a
 * @param {Transaction} t
 */
export const prepareTransaction = async (a: Account, t: Transaction) => {
  let fees = t.fees;
  fees = await getEstimatedFees({ a, t });
  if (!sameFees(t.fees, fees)) {
    return { ...t, fees };
  }

  return t;
};
Enter fullscreen mode Exit fullscreen mode

Validando Transações

Nós absolutamente queremos evitar que os usuários assinem transações que não atendem aos requisitos da blockchain, e seriam rejeitadas - ou pior - falhando, arriscando perder fundos.

Assim, validaremos todos os parâmetros que possam levar a transações inválidas:

Por exemplo, verificaremos se:

  • o destinatário não está vazio
  • o endereço do destinatário é válido
  • o destinatário existe
  • o usuário tem fundos suficientes para a transação o
  • usuário tem fundos suficientes para pagar as taxas de transação
  • o valor é estritamente positivo (evitando o envio de zero)
  • saldo mínimo ou depósito existente é respeitado se for relevante
  • etc

Esta validação é feita toda vez que o usuário atualiza a transação (qualquer mudança de entrada) para lhe dar um feedback imediato e contextual. O objeto de status retornado pelo getTransactionStatus segue esta definição:

  • errors: { [string]: Error }: potencial erro para cada campo (usuário) da transação
  • warnings: { [string]: Error }: potencial aviso para cada campo (usuário) para uma transação
  • estimatedFees: BigNumber: taxas totais estimadas que o tx vai custar (na moeda mainAccount)
  • ammount: BigNumber: valor real que o destinatário receberá (na moeda da conta)
  • totalSpent: BigNumber: valor total que o remetente gastará ( na moeda da conta)
  • recipientIsReadOnly?: boolean: o destinatário deve ser não editável

errors e warnings são objetos chave-valor (erro) que teriam para cada entrada (na perspectiva do usuário) o eventual erro ou aviso que foi detectado. Para cada chave, espera-se que exista uma entrada no Ledger Live Desktop e Mobile para exibir o erro.

libs/ledger-live-common/src/families/mycoin/js-getTransactionStatus.ts:



import { BigNumber } from "bignumber.js";
import {
  NotEnoughBalance,
  RecipientRequired,
  InvalidAddress,
  FeeNotLoaded,
} from "@ledgerhq/errors";
import type { Account, TransactionStatus } from "../../types";
import type { Transaction } from "./types";
import { isValidAddress, specificCheck } from "./logic";
import { MyCoinSpecificError } from "./errors";
const getTransactionStatus = async (
  a: Account,
  t: Transaction
): Promise<TransactionStatus> => {
  const errors = {};
  const warnings = {};
  const useAllAmount = !!t.useAllAmount;
  if (!t.fees) {
    errors.fees = new FeeNotLoaded();
  }

  const estimatedFees = t.fees || BigNumber(0);
  const totalSpent = useAllAmount
    ? a.balance
    : BigNumber(t.amount).plus(estimatedFees);
  const amount = useAllAmount
    ? a.balance.minus(estimatedFees)
    : BigNumber(t.amount);

  if (totalSpent.gt(a.balance)) {
    errors.amount = new NotEnoughBalance();
  }
  // If MyCoin needs any specific requirement on amount for instance
  if (specificCheck(t.amount)) {
    errors.amount = new MyCoinSpecificError();
  }

  if (!t.recipient) {
    errors.recipient = new RecipientRequired();
  } else if (isValidAddress(t.recipient)) {
    errors.recipient = new InvalidAddress();
  }
  return Promise.resolve({
    errors,
    warnings,
    estimatedFees,
    amount,
    totalSpent,
  });

};

export default getTransactionStatus;
Enter fullscreen mode Exit fullscreen mode

Lidando com Lógica e Erros

Conforme visto na seção anterior, getTransactionStatus lida com erros em uma Transação, devido a uma entrada errada do usuário que não atende à lógica da moeda. Esses são erros dependentes do usuário tratados na interface do usuário para cada entrada exibida e devem ocorrer de tempos em tempos - não estamos lançando-os.

Mas alguns erros podem ocorrer em um contexto diferente e não serem causados ​​pelo usuário. Tente o máximo possível lidar com todos os casos de falha e lançar erros específicos da moeda (se não existir um erro genérico), que você pode definir em um errors.ts (para reutilização).

libs/ledger-live-common/src/families/mycoin/errors.ts:

import { createCustomErrorClass } de "@ledgerhq/errors";
import { createCustomErrorClass } from "@ledgerhq/errors";
/**
 * MyCoin error thrown on a specifc check done on a transaction amount
 */
export const MyCoinSpecificError = createCustomErrorClass(
  "MyCoinSpecificError"
);
Enter fullscreen mode Exit fullscreen mode

Adicionar todas as exportações para src/errors.ts:

// ...
export * from "./families/mycoin/errors";
Enter fullscreen mode Exit fullscreen mode

Além disso, para evitar a repetição de código e facilitar o uso de verificações e constantes, reúna todas as suas funções lógicas específicas de moedas em um único arquivo (cálculos, getters, verificações booleanas…). Isso também facilitará a manutenção, por exemplo, quando a lógica da blockchain mudar (constantes ou verificações adicionais adicionadas).

libs/ledger-live-common/src/families/mycoin/logic.ts:

import { BigNumber } from "bignumber.js";
import type { Account } from "../../types";
export const MAX_AMOUNT = 5000;
/**
 * Returns true if address is a valid md5
 *
 * @param {string} address
 */
export const isValidAddress = (address: string): boolean => {
  if (!address) return false;

  return !!address.match(/^[a-f0-9]{32}$/);
};

/**
 * Returns true if transaction amount is less than MAX AMOUNT and > 0
 *
 * @param {BigNumber} amount
 */
export const specificCheck = (amount: BigNumber): boolean => {
  return amount.gt(0) && amount.lte(MAX_AMOUNT);
};

/**
 * Returns nonce for an account
 *
 * @param {Account} a
 */
export const getNonce = (a: Account): number => {
  const lastPendingOp = a.pendingOperations[0];

  const nonce = Math.max(
    a.myCoinResources?.nonce || 0,
    lastPendingOp && typeof lastPendingOp.transactionSequenceNumber === "number"
      ? lastPendingOp.transactionSequenceNumber + 1
      : 0
  );

  return nonce;
};
Enter fullscreen mode Exit fullscreen mode

Transação de construção e assinatura

O Transaction não é exatamente a transação na forma do protocolo da blockchain (que geralmente é serializado em um blob de bytes). Portanto, por conveniência, você pode implementar um buildTransaction para serializá-lo usando o MyCoin SDK, que pode ser reutilizado, por exemplo, para estimar taxas por meio da API.

libs/ledger-live-common/src/families/mycoin/js-buildTransaction.ts:

import type { Transaction } from "./types";
import type { Account } from "../../types";

import { getNonce } from "./logic";

const getTransactionParams = (a: Account, t: Transaction) => {
  switch (t.mode) {
    case "send":
      return t.useAllAmount
        ? {
            method: "transferAll",
            args: {
              dest: t.recipient,
            },
          }
        : {
            method: "transfer",
            args: {
              dest: t.recipient,
              value: t.amount.toString(),
            },
          };
    default:
      throw new Error("Unknown mode in transaction");
  }
};

/**
 *
 * @param {Account} a
 * @param {Transaction} t
 */
export const buildTransaction = async (a: Account, t: Transaction) => {
  const address = a.freshAddress;
  const params = getTransactionParams(a, t);
  const nonce = getNonce(a);

  const unsigned = {
    address,
    nonce,
    params,
  };

  // Will likely be a call to MyCoin SDK
  return JSON.stringify(unsigned);
};
Enter fullscreen mode Exit fullscreen mode

Esta buildTransaction retornaria um blob de transação não assinada que seria assinada com o MyCoin App no dispositivo:

libs/ledger-live-common/src/families/mycoin/js-signOperation.ts:

import { BigNumber } from "bignumber.js";
import { Observable } from "rxjs";
import { FeeNotLoaded } from "@ledgerhq/errors";

import type { Transaction } from "./types";
import type { Account, Operation, SignOperationEvent } from "../../types";

import { withDevice } from "../../hw/deviceAccess";
import { encodeOperationId } from "../../operation";
import MyCoin from "./hw-app-mycoin/MyCoin";

import { buildTransaction } from "./js-buildTransaction";
import { getNonce } from "./logic";

const buildOptimisticOperation = (
  account: Account,
  transaction: Transaction,
  fee: BigNumber
): Operation => {
  const type = "OUT";

  const value = BigNumber(transaction.amount).plus(fee);

  const operation: $Exact<Operation> = {
    id: encodeOperationId(account.id, "", type),
    hash: "",
    type,
    value,
    fee,
    blockHash: null,
    blockHeight: null,
    senders: [account.freshAddress],
    recipients: [transaction.recipient].filter(Boolean),
    accountId: account.id,
    transactionSequenceNumber: getNonce(account),
    date: new Date(),
    extra: { additionalField: transaction.amount },
  };

  return operation;
};

/**
 * Adds signature to unsigned transaction. Will likely be a call to MyCoin SDK
 */
const signTx = (unsigned: string, signature: any) => {
  return `${unsigned}:${signature}`;
};

/**
 * Sign Transaction with Ledger hardware
 */
const signOperation = ({
  account,
  deviceId,
  transaction,
}: {
  account: Account,
  deviceId: *,
  transaction: Transaction,
}): Observable<SignOperationEvent> =>
  withDevice(deviceId)((transport) =>
    Observable.create((o) => {
      async function main() {
        o.next({
          type: "device-signature-requested",
        });

        if (!transaction.fees) {
          throw new FeeNotLoaded();
        }

        const unsigned = await buildTransaction(account, transaction);

        // Sign by device
        const myCoin = new MyCoin(transport);
        const r = await myCoin.signTransaction(
          account.freshAddressPath,
          unsigned
        );

        const signed = signTx(unsigned, r.signature);

        o.next({ type: "device-signature-granted" });

        const operation = buildOptimisticOperation(
          account,
          transaction,
          transaction.fees ?? BigNumber(0)
        );

        o.next({
          type: "signed",
          signedOperation: {
            operation,
            signature: signed,
            expirationDate: null,
          },
        });
      } 

    main().then(
      () => o.complete(),
      (e) => o.error(e)
    );
  })
);

export default signOperation;
Enter fullscreen mode Exit fullscreen mode

A signOperation retorna um Observável que notificará seu assinante quando o usuário gratificar a assinatura. Ele deve notificá-lo com o signOperation, com assinatura (geralmente contém todo o blob a ser transmitido) e operação.

Esta operação é uma versão otimizada da Operação que seria exibida no histórico da conta como uma “Operação Pendente” se a transmissão fosse bem-sucedida. Esta operação pendente é importante para dar feedback ao usuário, mas também pode ser necessária para calcular o nonce (se relevante).

Os ajudantes de front-end

Live Common são principalmente dedicados a serem usados ​​pelos front-ends do Ledger Live (Desktop e Mobile), portanto, também contém utilitários para reagir e exibir dados específicos de criptografia.

Campos da transação do dispositivo

Ao assinar uma transação, o usuário vê em seu dispositivo todos os parâmetros desta transação através de múltiplas telas, que ele deve verificar em relação ao valor inserido e comparar com o que o Ledger Live está apresentando.

A lista de todos os campos exibidos no dispositivo é fornecida pela getDeviceTransactionConfig , que deve retornar todos os campos de transação de uma determinada transação.

libs/ledger-live-common/src/families/mycoin/deviceTransactionConfig.ts:

import type { AccountLike, Account, TransactionStatus } from "../../types";
import type { Transaction } from "./types";
import type { DeviceTransactionField } from "../../transaction";

function getDeviceTransactionConfig({
  transaction,
  status: { estimatedFees },
}: {
  account: AccountLike;
  parentAccount?: Account;
  transaction: Transaction;
  status: TransactionStatus;
}): Array<DeviceTransactionField> {
  const fields: Array<DeviceTransactionField> = [];

  if (transaction.useAllAmount) {
    fields.push({
      type: "text",
      label: "Method",
      value: "Transfer All",
    });
  } else {
    fields.push({
      type: "text",
      label: "Method",
      value: "Transfer",
    });
    fields.push({
      type: "amount",
      label: "Amount",
    });
  }

  if (!estimatedFees.isZero()) {
    fields.push({
      type: "fees",
      label: "Fees",
    });
  }

  return fields;
}

export default getDeviceTransactionConfig;
Enter fullscreen mode Exit fullscreen mode

Importante

Como um usuário mal informado pode ser enganado para assinar uma transação com destinatários errados, nunca mostramos os destinos nos aplicativos Ledger Live, para que os usuários se acostumem a sempre verificar externamente.

Dica

Se valores extras estiverem sendo calculados, certifique-se de que eles correspondam ao que aparece no dispositivo (ou seja, precisão numérica).

Transmissão

Uma vez que a transação é assinada, ela deve ser transmitida para a rede MyCoin. Isso é muito fácil se você encapsular corretamente sua API.

libs/ledger-live-common/src/families/mycoin/js-broadcast.ts
import type { Operation, SignedOperation } from "../../types";
import { patchOperationWithHash } from "../../operation";

import { submit } from "./api";

/**
 * Broadcast the signed transaction
 * @param {signature: string, operation: string} signedOperation
 */
const broadcast = async ({
  signedOperation: { signature, operation },
}: {
  signedOperation: SignedOperation,
}): Promise<Operation> => {
  const { hash } = await submit(signature);

  return patchOperationWithHash(operation, hash);
};

export default broadcast;
Enter fullscreen mode Exit fullscreen mode

Esta função deve retornar a operação otimizada, corrigida com o hash geralmente fornecido pela rede. Depois que a operação for sincronizada da MyCoin , o AccountBridge removerá essa operação otimizada do pendenteOperations.

Dica

Quando houver algumas operações pendentes, a sincronização ocorrerá a cada minuto.

Estimativa de gastos máximos

O valor máximo de gastos é o saldo total em uma conta que está disponível para envio em uma transação. Esse valor é específico para MyCoin, portanto, você precisará fornecer esse valor dependendo da transação que o usuário deseja enviar.

Consulte https://support.ledger.com/hc/en-us/articles/360012960679-Maximum-spendable-amount

libs/ledger-live-common/src/families/mycoin/js-estimateMaxSpendable.ts
import { BigNumber } from "bignumber.js";
import type { AccountLike, Account } from "../../types";
import { getMainAccount } from "../../account";
import type { Transaction } from "./types";
import { createTransaction } from "./js-transaction";
import getEstimatedFees from "./js-getFeesForTransaction";

/**
 * Returns the maximum possible amount for transaction
 *
 * @param {Object} param - the account, parentAccount and transaction
 */
const estimateMaxSpendable = async ({
  account,
  parentAccount,
  transaction,
}: {
  account: AccountLike,
  parentAccount?: Account,
  transaction?: Transaction,
}): Promise<BigNumber> => {
  const a = getMainAccount(account, parentAccount);
  const t = {
    ...createTransaction(),
    ...transaction,
    amount: a.spendableBalance,
  };

  const fees = await getEstimatedFees({ a, t });

  return a.spendableBalance.minus(fees);
};

export default estimateMaxSpendable;
Enter fullscreen mode Exit fullscreen mode

Dica

Se demorar muito para que as transações sejam confirmadas na cadeia e incluídas em uma sincronização, talvez seja necessário incluir as operações pendentes neste cálculo.

Aqui, devolvemos apenas o SpendableBalance, mas sem as taxas. Como a estimativa de taxa pode ser usada em outro lugar (como no prepareTransaction), você pode colocar sua lógica em um js-getFeesForTransaction.ts . Aqui está um exemplo de uma taxa obtida da rede de uma transação não assinada (um pouco como Polkadot), mas você também pode ter um cálculo específico, com o valor da taxa por byte fornecido pela blockchain.

libs/ledger-live-common/src/families/mycoin/js-getFeesForTransaction.ts:

import { BigNumber } from "bignumber.js";
import type { Account } from "../../types";
import type { Transaction } from "./types";
import { getFees } from "./api";
import { buildTransaction } from "./js-buildTransaction";

/**
 * Fetch the transaction fees for a transaction
 *
 * @param {Account} a
 * @param {Transaction} t
 */
const getEstimatedFees = async ({
  a,
  t,
}: {
  a: Account,
  t: Transaction,
}): Promise<BigNumber> => {
  const unsigned = await buildTransaction(a, t);
  const fees = await getFees(unsigned);

  return fees;
};

export default getEstimatedFees;
Enter fullscreen mode Exit fullscreen mode

Testando envio com CLI

Antes de poder testar um envio com CLI, você precisará vincular argumentos e inferir uma transação a partir dele. Como definimos um campo “modo” na transação, este será o único argumento necessário para testar um envio.

libs/ledger-live-common/src/families/mycoin/cli-transaction.ts:

import flatMap from "lodash/flatMap";
import type { Transaction, AccountLike } from "../../types";

const options = [
  {
    name: "mode",
    type: String,
    desc: "mode of transaction: send",
  },
];

function inferTransactions(
  transactions: Array<{ account: AccountLike, transaction: Transaction }>,
  opts: Object
): Transaction[] {
  return flatMap(transactions, ({ transaction, account }) => {
    if (!transaction.family !== "mycoin") {
      throw new Error("transaction is not of type mycoin");
    }

    if (account.type === "Account" && !account.myCoinResources) {
      throw new Error("unactivated account");
    }

    return {
      ...transaction,
      family: "mycoin",
      mode: opts.mode || "send",
    };
  });
}

export default {
  options,
  inferTransactions,
};
Enter fullscreen mode Exit fullscreen mode

Claro que se MyCoin tiver transações mais complexas, você pode adicionar muitos argumentos ao CLI. Você também pode definir seus próprios comandos CLI para quaisquer dados específicos que gostaria de buscar. Consulte Comandos CLI do Polkadot.

Agora você pode testar um getTransactionStatus ou um envio:



ledger-live getTransactionStatus -c mycoin -i 0 --amount 0.002 --recipient 8b2d58d4a7638e9ce8a0423bff5a2de0

# TRANSACTION
# SEND 0.0002 MYC
# TO 8b2d58d4a7638e9ce8a0423bff5a2de0
# STATUS
# amount: 0.0002 MYC
# estimated fees: 0.00157511 MYC
# total spent: 0,00177511 MYC

ledger-live send -c mycoin -i 0 --amount 0,002 --recipient 8b2d58d4a7638e9ce8a0423bff5a2de0

# {"type":"device-signature-requested"}
# {"type":"device-signature-granted "}
# {"id":"js:2:mycoin:0a93ac3773d54c77817e46e1007d66e3:-134ad522346bee108fa42a512788494e-OUT","hash":"134ad522346bee108fa42ablockHash":null,"blockHash":null,"blockHash":"OUT","type": ,"senders":["0a93ac3773d54c77817e46e1007d66e3"],"recipients":["8b2d58d4a7638e9ce8a0423bff5a2de0"],"accountId":"js:2:mycoin:0a93ac3773d54c77817e46e1007d66e3:","transactionSequenceNumber":0,"extra":{"additionalField" :"20000"},"date":"2021-03-01T08:59:57.445Z","value":"177511","fee":"157511"}


Enter fullscreen mode Exit fullscreen mode

Top comments (0)