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.
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 });
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;
// }
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);
};
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 };
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 };
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)