WEB3DEV

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

Posted on • Atualizado em

Adicionar Contas: Sincronização Completa - Ledger

Ponte de conta

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óstico na maneira como é executada, 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.

Importante
Se qualquer uma de suas operações demorar mais do que os tempos indicados abaixo, entre em contato conosco no Discord.

1

Receber

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

Sincronização

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

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

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á a toda Account que descobrir.

Com o auxiliar makeScanAccounts, você só precisa aprovar 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 libs/ledger-live-common/src/families/mycoin/js-postSyncPatch.js

Reconciliação

Atualmente, a 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:

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

Ponte de moeda

Verificando contas

Como vimos em Sincronização, o scanAccounts, que faz parte do CurrencyBridge, compartilha uma 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.

Pré-carregar dados de moeda (opcional)

Antes de criar ou usar uma ponte de moeda (por exemplo,para escanear contas ou para ser chamada a cada 2 minutos para sincronização), a Ledger Live tentará pré-carregar alguns dados de moeda (por exemplo, tokens, delegadores, etc.) necessário para que a função ledger-live-common funcione corretamente.

Esses dados pré-carregados serão armazenados em um cache persistente para uso futuro, para que a Ledger Live ainda funcione se estiver temporariamente offline e acelere a inicialização, dependendo do getPreloadStrategy (preloadMaxAge determina a expiração dos dados que acionará uma atualização).

Este cache contém a resposta seriada do JSON preload que é então fluida através do hydrate (que precisa de desserialização), diretamente após o pré-carregamento, ou após uma inicialização.

Os recursos do Live Common poderão então reutilizar esses dados em qualquer lugar (por exemplo, validando transações) com getCurrentMyCoinPreloadData, ou assinando observáveis getMyCoinPreloadDataUpdates.

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

import { Observable, Subject } from "rxjs";
import { log } from "@ledgerhq/logs";

import type { MyCoinPreloadData } from "./types";
import { getPreloadedData } from "./api";

const PRELOAD_MAX_AGE = 30 * 60 * 1000; // 30 minutes

let currentPreloadedData: MyCoinPreloadData = {
  somePreloadedData: {},
};

function fromHydratePreloadData(data: any): MyCoinPreloadData {
  let foo = null;

  if (typeof data === "object" && data) {
    if (typeof data.somePreloadedData === "object" && data.somePreloadedData) {
      foo = data.somePreloadedData.foo || "bar";
    }
  }

  return {
    somePreloadedData: { foo },
  };
}

const updates = new Subject<MyCoinPreloadData>();

export function getCurrentMyCoinPreloadData(): MyCoinPreloadData {
  return currentPreloadedData;
}

export function setMyCoinPreloadData(data: MyCoinPreloadData) {
  if (data === currentPreloadedData) return;

  currentPreloadedData = data;

  updates.next(data);
}

export function getMyCoinPreloadDataUpdates(): Observable<MyCoinPreloadData> {
  return updates.asObservable();
}

export const getPreloadStrategy = () => ({
  preloadMaxAge: PRELOAD_MAX_AGE,
});

export const preload = async (): Promise<MyCoinPreloadData> => {
  log("mycoin/preload", "preloading mycoin data...");

  const somePreloadedData = await getPreloadedData();

  return { somePreloadedData };
};

export const hydrate = (data: any) => {
  const hydrated = fromHydratePreloadData(data);

  log("mycoin/preload", `hydrated foo with ${hydrated.somePreloadedData.foo}`);

  setMyCoinPreloadData(hydrated);
};
Enter fullscreen mode Exit fullscreen mode

Leia mais sobre a documentação da Ponte de moeda.

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 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, envolvendo sua api e os diferentes arquivos.

O esqueleto de src/families/mycoin/bridge/js.ts deve 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.


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

Top comments (0)