WEB3DEV

Cover image for Adicionar Contas: Sincronização Leve - Ledger
Panegali
Panegali

Posted on • Atualizado em

Adicionar Contas: Sincronização Leve - Ledger

Tipos

Tipos genéricos podem ser encontrados em ledgerjs/packages/types-live, com alguma documentação na fonte.

Conta

Uma conta representa uma carteira onde um usuário detém ativos criptográficos para uma determinada moeda criptográfica.

O modelo Ledger Live é essencialmente uma matriz de Account porque muitas contas podem ser criadas, mesmo dentro de uma mesma moeda criptográfica.

Mais tecnicamente, uma conta é uma visão da blockchain no contexto de um usuário específico. Enquanto a blockchain rastreia a série de transações, movimentos de ativos que acontecem entre endereços, uma conta torna obrigatório isso do ponto de vista de um usuário que possui um conjunto de endereços e deseja visualizar os fundos associados que detém e poder realizar transações com ele.

Essencialmente, o que o usuário quer ver no final é seu saldo, um gráfico histórico e uma série de operações anteriores que foram realizadas. Passar da blockchain para o conceito de conta não é necessariamente trivial (em blockchains como a do Bitcoin, o conceito de conta não existe – você não cria ou destrói, esse conceito é uma visão, uma lente, que desconsideramos para o usuário).

No Live Common, existem atualmente 3 tipos de contas:

  • Account que é uma conta de nível superior associada a uma CryptoCurrency- a conta “principal”.
  • TokenAccount que é uma conta de nível aninhada, dentro de uma Conta e que está associada a um TokenCurrency.
  • ChildAccount que é uma variante de TokenAccount, mas mais no contexto de uma CryptoCurrency (normalmente usado para Tezos)

Eles são agregados como um único tipo AccountLike, usado nas implementações da Ledger Live.

Vamos nos concentrar apenas no tipo Account, pois não abordaremos a integração do Token neste documento.

Todas as contas principais compartilham um terreno comum, que você encontrará definido e comentado aqui.

Se necessário para a blockchain, uma conta também pode conter recursos específicos de moedas relacionados a uma única conta, como seu “nonce” ou saldos adicionais (por exemplo, para staking), ou qualquer coisa que possa ser exibida ou usada em sua implementação. Geralmente é um campo adicional como myCoinResources. Consulte abaixo os tipos específicos de família.

Operação

Observação

Este tipo não é necessário na etapa de sincronização leve e logo será removido desta seção

Observe que a palavra “Operação” é usada em vez de “Transação”. Uma conta não tem transações, tem operações.

A mesma abstração se aplica à Conta no topo da blockchain: uma Operação é uma visão de uma transação que aconteceu na blockchain que diz respeito à conta contextual. Está sempre no contexto de uma conta, ou seja, uma operação não existe por si só.

Na maioria das vezes, uma transação rende uma operação. Mas em algumas blockchains (como Ethereum) uma transação que diz respeito aos endereços associados à conta pode resultar em operações de 0 a N. Um exemplo típico são as transferências de contrato ou token (uma transação para mover um token gera uma operação de token e uma operação de taxa na conta ethereum). Outro exemplo que é possível na maioria das blockchains é que uma transação “self” rende duas operações (enviar, receber). Mas, na verdade, isso é semântico, nós, Ledger, colocamos. Também poderíamos ter permitido o conceito de operação “SELF”.

Resumindo, o histórico de transações na Ledger Live é uma lista de Operation, que estão confirmadas, não confirmadas ou pendentes (ainda não obtidas no explorer).

Todos eles compartilham o mesmo modelo, com um campo extra que pode armazenar quaisquer dados adicionais que você precise exibir. Você encontrará os campos detalhados e comentados aqui.

Se MyCoin tiver campos de operação específicos (como additionalField que adicionamos, por exemplo), você poderá exibi-los posteriormente. Eles não devem ser úteis em nenhum fluxo, apenas para interface do usuário.

Se MyCoin usa um “nonce”, então transactionSequenceNumber deve ser preenchido corretamente, pois será necessário para assinar novas transações (e será interpretado para limpar operações pendentes). No entanto, apenas a transação de saída deve ter esse valor.

Tipo de operação

Uma Operation tem um type que é uma string genérica digitada como OperationType, dando mais ou menos a direção da operação:

  • OUT: Uma transação de valor de envio/transferência
  • IN: Uma transação de valor recebido/entrante
  • FEES: uma transação que cobra apenas taxas
  • NONE: Uma transação que não afeta o saldo
  • REWARD: Uma recompensa reivindicada (como uma transação de saída com taxas)
  • REWARD_PAYOUT: Uma recompensa recebida (como uma transação recebida)
  • SLASH: Uma barra de staking (com quantidade reduzida em geral)

Existem mais tipos disponíveis, os existentes terão ícones, traduções e comportamentos predefinidos (ou seja getOperationAmountNumber(), em src/operation.ts).

MyCoin também pode ter tipos de operação específicos, se você precisar adicionar um tipo que ainda não foi implementado, adicione-os em libs/ledgerjs/packages/types-live/src/operation.ts. Mais tarde, você precisará implementar algum código específico para a Ledger Live Desktop e Mobile para exibi-los corretamente.

Você será solicitado a adicionar um svg para o ícone do seu tipo e um rótulo para ser traduzido, você pode verificar a operação “OPT_IN” na Algorand.

Em LLD:

src/renderer/components/OperationsList/ConfirmationCheck.js
Enter fullscreen mode Exit fullscreen mode

Em LLM: (Você precisará verificar se o SVG está envolvido em nosso componente)

src/icons/OperationStatusIcon/index.js
Enter fullscreen mode Exit fullscreen mode

Transação

Observação

Este modelo pode não ser necessário na etapa de sincronização leve.

Na Ledger Live, a “Transação” é o modelo de dados que é criado e atualizado para construir o blob final a ser assinado pelo dispositivo e depois transmitido para a blockchain.

É altamente específico para o protocolo blockchain e contém todos os dados que precisarão ser serializados no formato de transmissão da blockchain. Ele vive apenas por um curto período de tempo no aplicativo - durante o qual o usuário está escolhendo os parâmetros de sua transação antes de ser assinado e enviado para a blockchain, após o qual será transformado em uma operação.

Observe que esta Operação será considerada “pendente”, pois é uma versão otimista da Operação real que aparecerá no histórico da conta, após ser sincronizada.

Transaction conterá todos os dados necessários para criar uma transação na blockchain. Ela é criada assim que o usuário inicia um fluxo de transação na Ledger Live, e será atualizado de acordo com suas entradas, como o valor ou a escolha de um destinatário.

Ele compartilha uma forma comum entre todas as moedas, às quais adicionaremos campos específicos:

  • amount: BigNumber: valor analisado da entrada do usuário - será 0 se o usuário usar todo o valor
  • recipient: string: o destinatário deste montante
  • useAllAmount?: boolean: indicado se o usuário deseja transferir todo o saldo disponível
  • subAccountId?: ?string: o ID da conta filho/token, se relevante
  • family: string: a família de moedas da transação (como um discriminador de moedas)

Algumas moedas também possuem alguns campos geralmente respeitando a mesma semântica, aqui estão alguns fornecidos como exemplo:

  • mode: string: tipo de transação a ser transmitida ( "send", "freeze", "delegate", "claimReward"…)
  • fees: BigNumber: taxas (fornecidas pela blockchain ou preenchidas pelo usuário)
  • memo: ?string: memorando se exigido pela troca

Embora alguns campos sejam obrigatórios, eles podem ser esvaziados (recebedor = “” e valor = 0) e ignorados para transações que não os utilizam.

Você deve adicionar todos os campos que seriam exigidos pelo MyCoin para serem transmitidos corretamente - respeitando o máximo possível o léxico de seu protocolo.

Veja as implementações existentes para se inspirar: tipos Polkadot, tipos Cosmos

Tipos específicos de família

Você estará implementando tipos de typescript que serão usados ​​em sua integração, como o tipo de transação ou os dados adicionais necessários para serem adicionados ao tipo compartilhado de conta, mas também quaisquer outros tipos que você precise (lembre-se de sempre digitar suas funções com typescript).

libs/ledger-live-common/src/families/mycoin/types.ts:
Enter fullscreen mode Exit fullscreen mode
import type { BigNumber } from "bignumber.js";
import type {
  TransactionCommon,
  TransactionCommonRaw,
} from "../../types/transaction";

/**
 * Recursos da conta MyCoin
 */
export type MyCoinResources = {
  nonce: number;
  additionalBalance: BigNumber;
};

/**
 * Recursos da conta MyCoin do JSON bruto
 */
export type MyCoinResourcesRaw = {
  nonce: number;
  additionalBalance: string;
};

/**
 * Transação MyCoin
 */
export type Transaction = TransactionCommon & {
  mode: string;
  family: "mycoin";
  fees?: BigNumber;
  // adicionar aqui todos os campos específicos da transação se você implementar outros modos além de "enviar".
};

/**
 * Transação MyCoin a partir de um JSON bruto
 */
export type TransactionRaw = TransactionCommonRaw & {
  family: "mycoin";
  mode: string;
  fees?: string;
  // também os campos de transação como dados brutos do JSON
};

/**
 * Dados de moeda MyCoin que serão pré-carregados.
 * Você pode, por exemplo, adicionar uma lista de validadores para blockchains Proof-of-Stake,
 * ou quaisquer dados voláteis que não puderam ser definidos como constantes no código (progresso de staking, variáveis de estimativa de taxas, etc.)
 */
export type MyCoinPreloadData = {
  somePreloadedData: Record<any, any>;
};
Enter fullscreen mode Exit fullscreen mode

Como alguns desses tipos serão serializados quando armazenados ou armazenados em cache, pode ser necessário definir funções de serialização/desserialização para aqueles:

libs/ledger-live-common/src/families/mycoin/serialization.ts
Enter fullscreen mode Exit fullscreen mode
import { BigNumber } from "bignumber.js";
import type { MyCoinResourcesRaw, MyCoinResources } from "./types";

export function toMyCoinResourcesRaw(r: MyCoinResources): MyCoinResourcesRaw {
  const { nonce, additionalBalance } = r;
  return {
    nonce,
    additionalBalance: additionalBalance.toString(),
  };
}

export function fromMyCoinResourcesRaw(r: MyCoinResourcesRaw): MyCoinResources {
  const { nonce, additionalBalance } = r;
  return {
    nonce,
    additionalBalance: BigNumber(additionalBalance),
  };
}
Enter fullscreen mode Exit fullscreen mode

Como a conta é genérica, você pode precisar adicionar seus recursos específicos a libs/ledgerjs/packages/types-live/src/account.ts, se precisar armazenar informações específicas relacionadas à blockchain (como staking, validadores ou saldo congelado…) que não são tratadas na conta.

// ...
import type {
  MyCoinResources,
  MyCoinResourcesRaw,
} from "../families/mycoin/types";

// exportar tipo da Conta = {
// ...
// // Em alguma blockchains, uma conta pode ter recursos (obtidos, delegados, ...)
  myCoinResources?: MyCoinResources;
// };

// exportar tipo AccountRaw = {
  myCoinResources?: MyCoinResourcesRaw;
// };
Enter fullscreen mode Exit fullscreen mode

…e manusear a serialização associada src/account/serialization.ts(se você usar BigInt, precisará torná-lo bruto alterando-o para string, por exemplo):

// ...
import {
  toMyCoinResourcesRaw,
  fromMyCoinResourcesRaw,
} from "../families/mycoin/serialization";
// ...
export { toMyCoinResourcesRaw, fromMyCoinResourcesRaw };
// ...

// exportar função fromAccountRaw(rawAccount: AccountRaw): Account {
//   const {
//     ...
    myCoinResources,
// }
// ...
  if (myCoinResources) {
    res.myCoinResources = fromMyCoinResourcesRaw(myCoinResources);
  }
//   returnar res;
// }

// exportar função toAccountRaw({
// ...
  myCoinResources,
// }: Account): AccountRaw {
// ...
  if (myCoinResources) {
    res.myCoinResources = toMyCoinResourcesRaw(myCoinResources);
  }
//   returnar res;
// }
Enter fullscreen mode Exit fullscreen mode

Se a sua integração do MyCoin não exigir dados específicos da moeda em uma conta, você não precisará definir MyCoinResources.

Formato de apresentação

Como Operationserá armazenado como JSON, você precisará implementar serializadores específicos para o campo extra.

Também gostaríamos que Operation e Account fossem exibidos na CLI com suas especificidades, portanto, você deve fornecer formatadores para exibi-los.

Observação

Nesta amostra de código, todas as referências a operações não são necessárias na etapa de sincronização leve. Atualmente, elas são necessárias, mas em breve serão removidas desta seção

libs/ledger-live-common/src/families/mycoin/account.ts:
Enter fullscreen mode Exit fullscreen mode
import { BigNumber } from "bignumber.js";
import type { Account, Operation, Unit } from "../../types";
import { getAccountUnit } from "../../account";
import { formatCurrencyUnit } from "../../currencies";

function formatAccountSpecifics(account: Account): string {
  const { myCoinResources } = account;
  if (!myCoinResources) {
    throw new Error("mycoin account expected");
  }

  const unit = getAccountUnit(account);
  const formatConfig = {
    disableRounding: true,
    alwaysShowSign: false,
    showCode: true,
  };

  let str = " ";

  str +=
    formatCurrencyUnit(unit, account.spendableBalance, formatConfig) +
    " spendable. ";

  if (myCoinResources.additionalBalance.gt(0)) {
    str +=
      formatCurrencyUnit(
        unit,
        myCoinResources.additionalBalance,
        formatConfig
      ) + " additional. ";
  }

  if (myCoinResources.nonce) {
    str += "\nonce : " + myCoinResources.nonce;
  }

  return str;
}

function formatOperationSpecifics(op: Operation, unit: ?Unit): string {
  const { additionalField } = op.extra;

  let str = " ";

  const formatConfig = {
    disableRounding: true,
    alwaysShowSign: false,
    showCode: true,
  };

  str +=
    additionalField && !additionalField.isNaN()
      ? `\n    additionalField: ${
          unit
            ? formatCurrencyUnit(unit, additionalField, formatConfig)
            : additionalField
        }`
      : "";

  return str;
}

export function fromOperationExtraRaw(extra: ?Object) {
  if (extra && extra.additionalField) {
    extra = {
      ...extra,
      additionalField: BigNumber(extra.additionalField),
    };
  }
  return extra;
}

export function toOperationExtraRaw(extra: ?Object) {
  if (extra && extra.additionalField) {
    extra = {
      ...extra,
      additionalField: extra.additionalField.toString(),
    };
  }
  return extra;
}

export default {
  formatAccountSpecifics,
  formatOperationSpecifics,
  fromOperationExtraRaw,
  toOperationExtraRaw,
};
Enter fullscreen mode Exit fullscreen mode

formatOperationSpecifics() e formatAccountSpecifics()são usados ​​na CLI para exibir campos específicos da conta e extras do histórico de transações, úteis para depuração.

A mesma ideia se aplica também ao tipo Transaction que precisa ser serializado e formatado para CLI:

Observação

Esta parte pode não ser necessária se você não precisar de transações para a sincronização leve.

libs/ledger-live-common/src/families/mycoin/transaction.ts:
Enter fullscreen mode Exit fullscreen mode
import type { Transaction, TransactionRaw } from "./types";
import { BigNumber } from "bignumber.js";
import {
  fromTransactionCommonRaw,
  toTransactionCommonRaw,
} from "../../transaction/common";
import type { Account } from "../../types";
import { getAccountUnit } from "../../account";
import { formatCurrencyUnit } from "../../currencies";

export const formatTransaction = (
  { mode, amount, recipient, useAllAmount }: Transaction,
  account: Account
): string =>
  `
${mode.toUpperCase()} ${
    useAllAmount
      ? "MAX"
      : amount.isZero()
      ? ""
      : " " +
        formatCurrencyUnit(getAccountUnit(account), amount, {
          showCode: true,
          disableRounding: true,
        })
  }${recipient ? `\nTO ${recipient}` : ""}`;

export const fromTransactionRaw = (tr: TransactionRaw): Transaction => {
  const common = fromTransactionCommonRaw(tr);
  return {
    ...common,
    family: tr.family,
    mode: tr.mode,
    fees: tr.fees ? BigNumber(tr.fees) : null,
  };
};

export const toTransactionRaw = (t: Transaction): TransactionRaw => {
  const common = toTransactionCommonRaw(t);
  return {
    ...common,
    family: t.family,
    mode: t.mode,
    fees: t.fees?.toString() || null,
  };
};

export default { formatTransaction, fromTransactionRaw, toTransactionRaw };
Enter fullscreen mode Exit fullscreen mode

Embrulhe seu API

Observação

Nas amostras de código abaixo, todas as referências a operações não são necessárias na etapa de sincronização leve. Atualmente, elas são necessárias, mas em breve serão removidas desta seção.

Antes desta parte, você precisará de um nó/explorador para obter o estado de uma conta na blockchain, como saldos, nonce (se sua blockchain usar algo semelhante) e quaisquer dados relevantes para mostrar ou buscar na Ledger Live.

Para o exemplo, vamos supor que o MyCoin fornece um SDK capaz de obter o estado e o histórico da conta.

A melhor maneira de implementar seu API na Live Common é criar uma subpasta api exclusiva, que exporte todas as funções que exigem chamadas para terceiros e mapeie suas respostas para os tipos Ledger Live.

./src/families/mycoin
└── api
  ├── index.ts
  ├── sdk.ts
  └── sdk.types.ts
Enter fullscreen mode Exit fullscreen mode

Dica

Tente separar ao máximo seus diferentes APIs (se você usar vários provedores) e use digitação para garantir que você mapeie corretamente as respostas de API para os tipos Ledger Live

Você provavelmente precisará exportar essas funções, mas a implementação depende do desenvolvedor:

libs/ledger-live-common/src/families/mycoin/api/index.ts:
Enter fullscreen mode Exit fullscreen mode
export {
  getAccount,
  getOperations,
  getPreloadedData, // ajustar com as necessidades de dados pré-carregados
  getFees,
  submit,
  disconnect, // se usando conexão persistente
} from "./sdk";
Enter fullscreen mode Exit fullscreen mode

Basicamente, nas próximas seções, getAccount será chamada para criar uma Account com saldos e eventuais recursos adicionais, e getOperations será chamado para preencher o operations[] desta conta, com todo o histórico de operações que podem ser solicitados de forma incremental. Em seguida, antes de enviar uma transação getFees , informe ao usuário o custo da rede (estimado ou efetivo) e submit transmita sua transação após a assinatura.

Veja a api da Polkadot Coin Integration para uma boa inspiração.

Exemplo de API

Aqui está um exemplo de implementação de API MyCoin usando um SDK fictício que usa algo como WebSocket para buscar dados.

Dica

Não recomendamos o uso do WebSocket, pois ele pode ter inconvenientes e é mais difícil de ser escalado e colocado atrás de um proxy reverso. Se possível, use solicitações HTTPS o máximo que puder.

import MyCoinApi from "my-coin-sdk";
import type { MyCoinTransaction } from "my-coin-sdk";
import { BigNumber } from "bignumber.js";

import { getEnv } from "../../../env";
import { encodeOperationId } from "../../../operation";

type AsyncApiFunction = (api: typeof MyCoinApi) => Promise<any>;

const MYCOIN_API_ENDPOINT = () => getEnv("MYCOIN_API_ENDPOINT");

let api = null;

/**
 * Conecta-se ao Api MyCoin
 */
async function withApi(execute: AsyncApiFunction): Promise<any> {
  // Se o cliente já estiver instantâneo, certifique-se de que esteja conectado & pronto
  if (api) {
    try {
      await api.isReady;
    } catch (err) {
      api = null;
    }
  }

  if (!api) {
    api = new MyCoinApi(MYCOIN_API_ENDPOINT);
    api.connect();
    api.onClose(() => {
      api = null;
    });
    await api.isReady;
  }

  try {
    const res = await execute(api);

    return res;
  } catch {
    // Lidar com erro ou tentar novamente
    await disconnect();
  }
}

/**
 * Desconecta o Api MyCoin
 */
export const disconnect = async () => {
  if (api) {
    const disconnecting = api;
    api = null;
    await disconnecting.close();
  }
};

/**
 * Obter saldos de conta e nonce
 */
export const getAccount = async (addr: string) =>
  withApi(async (api) => {
    const [balance, nonce, blockHeight] = await Promise.all([
      api.getBalance(addr),
      api.getTransactionCount(addr),
      api.getBlockNumber(),
    ]);

    return {
      blockHeight,
      balance: BigNumber(balance),
      additionalBalance: BigNumber(additionalBalance),
      nonce,
    };
  });

/**
 * Retorna verdadeiro se a conta for o signatário
 */
function isSender(transaction: MyCoinTransaction, addr: string): boolean {
  return transaction.sender === addr;
}

/**
 * Mapear transação para um tipo de operação
 */
function getOperationType(
  transaction: MyCoinTransaction,
  addr: string
): OperationType {
  return isSender(transaction, addr) ? "OUT" : "IN";
}

/**
 * Mapear a transação para um valor de operação correto (afetando o saldo da conta)
 */
function getOperationValue(
  transaction: MyCoinTransaction,
  addr: string
): BigNumber {
  return isSender(transaction, addr)
    ? BigNumber(transaction.value).plus(transaction.fees)
    : BigNumber(transaction.value);
}

/**
 * Extrair extra da transação, se houver
 */
function getOperationExtra(transaction: MyCoinTransaction): Object {
  return {
    additionalField: transaction.additionalField,
  };
}

/**
 * Mapear a transação do histórico do MyCoin para uma operação Ledger Live
 */
function transactionToOperation(
  accountId: string,
  addr: string,
  transaction: MyCoinTransaction
): Operation {
  const type = getOperationType(transaction, addr);

  return {
    id: encodeOperationId(accountId, transaction.hash, type),
    accountId,
    fee: BigNumber(transaction.fees || 0),
    value: getOperationValue(transaction, addr),
    type,
    // Aqui é onde você recupera o hash da transação
    hash: transaction.hash,
    blockHash: null,
    blockHeight: transaction.blockNumber,
    date: new Date(transaction.timestamp),
    extra: getOperationExtra(transaction),
    senders: [transaction.sender],
    recipients: transaction.recipient ? [transaction.recipient] : [],
    transactionSequenceNumber: isSender(transaction, addr)
      ? transaction.nonce
      : undefined,
    hasFailed: !transaction.success,
  };
}

/**
 * Buscar lista de operações
 */
export const getOperations = async (
  accountId: string,
  addr: string,
  startAt: number
): Promise<Operation[]> =>
  withApi(async (api) => {
    const rawTransactions = await api.getHistory(addr, startAt);

    return rawTransactions.map((transaction) =>
      transactionToOperation(accountId, addr, transaction)
    );
  });
Enter fullscreen mode Exit fullscreen mode

Se você precisar se desconectar da sua API após usá-la, atualize src/api/index.ts para adicionar sua desconexão da api na função disconnectAll, isso evitará que testes e CLI travem.

Account Bridge

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

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

1

Receive

O método receive permite o endereço derivado de uma conta com um dispositivo Nano, mas também o exibe no dispositivo se a verificação for aprovada. Como você pode ver em src/families/mycoin/bridge/js.ts, o Live Common fornece um ajudante para implementá-lo facilmente com o makeAccountBridgeReceive(), e há muito poucos motivos para implementar o seu próprio.

Sincronização

Geralmente agrupamos scanAccounts e sync no mesmo arquivo js-synchronisation.ts, pois ambos usam lógica semelhante como uma função getAccountShape passada para auxiliares.

src/families/mycoin/js-synchronisation.ts:
Enter fullscreen mode Exit fullscreen mode
import type { Account } from "../../types";
import type { GetAccountShape } from "../../bridge/jsHelpers";
import { makeSync, makeScanAccounts, mergeOps } from "../../bridge/jsHelpers";

import {
  encodeAccountId
} from "../../account";

import { getAccount, getOperations } from "./api";

const getAccountShape: GetAccountShape = async (info) => {
  const { id, address, initialAccount } = info;
  const oldOperations = initialAccount?.operations || [];

  // Necessário para sincronização incremental
  const startAt = oldOperations.length
    ? (oldOperations[0].blockHeight || 0) + 1
    : 0;

  const accountId = encodeAccountId({
    type: "js",
    version: "2",
    currencyId: currency.id,
    xpubOrAddress: address,
    derivationMode,
  });

  // obter o estado do saldo da conta corrente dependendo de sua implementação api
  const { blockHeight, balance, additionalBalance, nonce } = await getAccount(
    address
  );

  // Mesclar novas operações com as anteriormente sincronizadas
  const newOperations = await getOperations(id, address, startAt);
  const operations = mergeOps(oldOperations, newOperations);

  const shape = {
    id,
    balance,
    spendableBalance: balance,
    operationsCount: operations.length,
    blockHeight,
    myCoinResources: {
      nonce,
      additionalBalance,
    },
  };

  return { ...shape, operations };
};

const postSync = (initial: Account, parent: Account) => parent;

export const scanAccounts = makeScanAccounts({ getAccountShape });
export const sync = makeSync({ getAccountShape, postSync });
Enter fullscreen mode Exit fullscreen mode

A função scanAccounts realiza a derivação de endereços para um dado currency e deviceId, e retorna um Observável que notificará toda Account que descobrir.

Com o auxiliar makeScanAccounts, você só precisa passar uma função getAccountShape para executar a varredura genérica que a Ledger Live usa com os modos de derivação corretos para MyCoin, e ela determinará quando parar (geralmente assim que uma conta vazia for encontrada).

A função sync realiza uma “sincronização de conta” que consiste em atualizar todos os campos de uma conta (criada anteriormente) da sua API.

Ela é executada a cada 2 minutos se tudo funcionar como esperado, mas se falhar, uma estratégia de repetição será executada com um atraso crescente (para evitar sobrecarregar uma API com falha).

Sob o capô do auxiliar makeSync , o valor retornado é um Observável de uma função de atualização (Account=>Account), que é um padrão com algumas vantagens:

  • evita condições de corrida
  • o atualizador é chamado em um redutor e permite produzir um estado imutável aplicando a atualização à instância da conta mais recente (com reconciliação na Ledger Live Desktop)
  • é um observável, então podemos interrompê-lo quando/se ocorrerem várias atualizações

Em alguns casos, pode ser necessário fazer uma correção postSync para adicionar alguma lógica de atualização após a sincronização (antes da reconciliação que ocorre na Ledger Live Desktop). Se esta função postSync for complexa, você deve dividir esta função em um arquivo src/families/mycoin/js-postSyncPatch.js.

Reconciliação

Atualmente, o Ledger Live Desktop executa essa ponte em uma linha separada. Assim, o aspecto "evitar condição de corrida" da sincronização pode não ser respeitado, pois a linha do renderizador da interface do usuário não compartilha os mesmos objetos. Isso pode ser melhorado no futuro, mas para que as atualizações sejam refletidas durante a sincronização, implementamos a reconciliação em src/reconciliation.js, entre a conta que está no renderizador e a nova conta produzida após a sincronização.

Como podemos ter adicionado alguns dados específicos de moedas em Account, também devemos reconciliá-los:

src/reconciliation:
Enter fullscreen mode Exit fullscreen mode
// importar {
// ...
  fromMyCoinResourcesRaw,
// } de "./account";
// ...
// exportar função patchAccount(
//   conta: Account,
//   updatedRaw: AccountRaw
// ): Account {
// ...
  if (
    updatedRaw.myCoinResources &&
    account.myCoinResources !== updatedRaw.myCoinResources
  ) {
    next.myCoinResources = fromMyCoinResourcesRaw(
      updatedRaw.myCoinResources
    );
    changed = true;
  }
//   se (!changed) returnar conta; // nada mudou em absoluto
//
//   retornar em seguida;
// }
Enter fullscreen mode Exit fullscreen mode

Ponte para moedas

Verificando contas

Como vimos em Sincronização, o scanAccounts, que faz parte do CurrencyBridge, compartilha lógica comum com a função sync, por isso preferimos colocá-los em um arquivo js-synchronisation.ts.

O auxiliar makeScanAccounts executará automaticamente a lógica de derivação de endereço padrão, mas por algum motivo, se você precisar ter uma maneira completamente nova de verificar a conta, poderá implementar sua própria estratégia.

Ícone

Os ícones geralmente são mantidos pela equipe de design da Ledger, então você deve primeiro verificar se o ícone MyCoin ainda não foi adicionado em ledger-live-common, em src/data/icons/svg. Ele contém versões limpas de ícones de criptomoedas de cryptoicons.co, organizados por ticker.

Se você precisar adicionar seus próprios, eles devem respeitar esses requisitos:

  • Limpe o SVG apenas com elementos <path> que representam a criptomoeda
  • O tamanho e a janela de visualização devem ser 24x24
  • O ícone deve ser 18x18 centralizado/preenchido
  • Estilo plano e deve respeitar o esquema de cores criptográfico (preenchido)
  • Sem fundo ou forma decorativa adicionada
  • Sem <g> ou atributos transform,style

O nome deve ser o ticker da moeda (por exemplo MYC.svg,) e não deve entrar em conflito com uma moeda ou token existente.

Ao construir ledger-live-common, um script os converte automaticamente em componentes React e React Native.

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. Ela está conectada a qualquer indexador/explorador e fornece uma ideia aproximada de como ela ficará quando conectada à interface do usuário.

Por exemplo, você pode usá-lo fazendo MOCK=1 pnpm dev:lld em 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

Divida seu código

Agora você pode começar a implementar a ponte JS para MyCoin. Pode precisar de algumas alterações entre os tipos, seu API empacotado e os diferentes arquivos.

O esqueleto de libs/ledger-live-common/src/families/mycoin/bridge/js.tsdeve ser algo assim:

import type { AccountBridge, CurrencyBridge } from "../../../types";
import type { Transaction } from "../types";
import { makeAccountBridgeReceive } from "../../../bridge/jsHelpers";

import { getPreloadStrategy, preload, hydrate } from "../preload";

import { sync, scanAccounts } from "../js-synchronisation";

const receive = makeAccountBridgeReceive();

const currencyBridge: CurrencyBridge = {
  getPreloadStrategy,
  preload,
  hydrate,
  scanAccounts,
};

const createTransaction = () => {
  throw new Error("createTransaction not implemented");
};

const prepareTransaction = () => {
  throw new Error("prepareTransaction not implemented");
};

const updateTransaction = () => {
  throw new Error("updateTransaction not implemented");
};

const getTransactionStatus = () => {
  throw new Error("getTransactionStatus not implemented");
};

const estimateMaxSpendable = () => {
  throw new Error("estimateMaxSpendable not implemented");
};

const signOperation = () => {
  throw new Error("signOperation not implemented");
};

const broadcast = () => {
  throw new Error("broadcast not implemented");
};

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

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

Dica

Você poderia implementar todos os métodos em um único arquivo, mas para uma melhor legibilidade e manutenção, você deve dividir seu código em vários arquivos.

Cache e desempenho (opcional)

É importante ter em mente que todas as moedas funcionam de forma independente e que o Live Common fornece uma estrutura comum para sincronizar contas com uma estratégia de pesquisa e que a conectividade de rede nem sempre é estável e ideal.

Portanto, quanto mais criptomoedas a Ledger Live estiver usando, mais solicitações e cálculos serão executados, o que pode levar tempo.

Para evitar fazer as mesmas solicitações várias vezes, recomendamos usar um cache local em sua implementação (por exemplo, estimativas de taxas, alguns dados de moeda para pré-carregar etc.) em um arquivo libs/ledger-live-common/src/families/mycoin/cache.ts.

Temos um auxiliar src/cache.ts para a criação de caches de uso menos frequente em qualquer lugar, se necessário.

Veja por exemplo a implementação de cache de Polkadot.

Ganchos React

Se você estiver adicionando recursos específicos a Ledger Live (como staking), pode ser necessário acessar dados por meio de ganchos React, que podem fornecer uma lógica comum reutilizável para componentes React.

Você está livre para adicioná-los em um arquivo libs/ledger-live-common/src/families/mycoin/react.ts.

Veja exemplos como validadores de classificação e filtragem, assinatura de dados observáveis ​​pré-carregados ou espera que uma transação seja refletida na conta, nos ganchos React de Polkadot.


Este artigo foi publicado no Portal do desenvolvedor Ledger. Traduzido por Marcelo Panegali.

Top comments (0)