Tiny devices made of DNA detect cancer with fewer false alarms
DNA circuits could help ensure that cancer screens and therapies zero in on the right culprits
| Source: | Duke University |
| Summary: | A new cancer-detecting tool uses tiny circuits made of DNA to identify cancer cells by the molecular signature on their surface. The circuits work by attaching to the outside of a cell and analyzing it for proteins that are more abundant on some cell types than others. The devices distinguish cell types with higher specificity than previous methods, giving researchers hope their work could improve diagnosis, or give cancer therapies better aim. |
A new cancer-detecting tool uses tiny circuits made of DNA to identify cancer cells by the molecular signatures on their surface.
Duke University researchers fashioned the simple circuits from interacting strands of synthetic DNA that are tens of thousands of times finer than a human hair.
Unlike the circuits in a computer, these circuits work by attaching to the outside of a cell and analyzing it for proteins found in greater numbers on some cell types than others. If a circuit finds its targets, it labels the cell with a tiny light-up tag.
Because the devices distinguish cell types with higher specificity than previous methods, the researchers hope their work might improve diagnosis, and give cancer therapies better aim.
A team led by Duke computer scientist John Reif and his former Ph.D. student Tianqi Song described their approach in a recent issue of the Journal of the American Chemical Society.
Similar techniques have been used previously to detect cancer, but they're more prone to false alarms — misidentifications that occur when mixtures of cells sport one or more of the proteins a DNA circuit is designed to screen for, but no single cell type has them all.
For every cancer cell that is correctly detected using current methods, some fraction of healthy cells also get mislabeled as possibly cancerous when they're not.
Each type of cancer cell has a characteristic set of cell membrane proteins on its cell surface. To cut down on cases of mistaken identity, the Duke team designed a DNA circuit that must latch onto that specific combination of proteins on the same cell to work.
As a result they're much less likely to flag the wrong cells, Reif said.
The technology could be used as a screening tool to help rule out cancer, which could mean fewer unnecessary follow-ups, or to develop more targeted cancer treatments with fewer side effects.
Each basic element of their DNA circuit consists of two DNA strands. The first DNA strand folds over and partially pairs up with itself to form a hairpin shape. One end of each hairpin is bound to a second strand of DNA that acts as a lock and tether, folding in such a way to fit a specific cell surface protein like a puzzle piece. Together these two strands act to verify that that particular protein is present on the cell surface.
To look for cancer, the circuit components are mixed with a person's cells in the lab. If any cells are studded with the right combination of proteins, the complete circuit will attach. Adding a strand of "initiator" DNA then causes one of the hairpins to open, which in turn triggers another in a chain reaction until the last hairpin in the circuit is opened and the cell lights up.
Test runs of the device in test tubes in Reif's lab showed it can be used to detect leukemia cells and to distinguish them from other types of cancer within a matter of hours, just by the strength of their glow.
The devices can be easily reconfigured to detect different cell surface proteins by replacing the tether strands, the researchers say. In the future, Reif plans to the DNA circuits to release a small molecule that alerts the body's immune system to attack the cancer cell.
The technology isn't ready for prime time yet. The researchers say their DNA circuits require testing in more realistic conditions to make sure they still flag the right cells.
But it's a promising step toward ensuring that cancer screens and therapies zero in on the right culprits.
This research was supported by grants from the National Science Foundation (CCF-1320360, CCF-1217457, CCF-1617791, CCF-1813805).
make a difference: sponsored opportunity
Story Source:
Materials provided by Duke University. Original written by Robin Ann Smith.
Note: Content may be edited for style and length.
Journal Reference:
- Tianqi Song, Shalin Shah, Hieu Bui, Sudhanshu Garg, Abeer Eshra, Daniel Fu, Ming Yang, Reem Mokhtar, John Reif. Programming DNA-Based Biomolecular Reaction Networks on Cancer Cell Membranes. Journal of the American Chemical Society, 2019; 141 (42): 16539 DOI: 10.1021/jacs.9b05598
Cite This Page:
| MLA | Duke University. "Tiny devices made of DNA detect cancer with fewer false alarms: DNA circuits could help ensure that cancer screens and therapies zero in on the right culprits." ScienceDaily. ScienceDaily, 22 November 2019. <www.sciencedaily.com/releases/2019/11/191122122511.htm>. |
| APA | Duke University. (2019, November 22). Tiny devices made of DNA detect cancer with fewer false alarms: DNA circuits could help ensure that cancer screens and therapies zero in on the right culprits. ScienceDaily. Retrieved November 25, 2019 from www.sciencedaily.com/releases/2019/11/191122122511.htm |
| Chicago | Duke University. "Tiny devices made of DNA detect cancer with fewer false alarms: DNA circuits could help ensure that cancer screens and therapies zero in on the right culprits." ScienceDaily. www.sciencedaily.com/releases/2019/11/191122122511.htm (accessed November 25, 2019). |


Hello World
About Page
Home
About
welcome to grunt build system template
clone or download zip
cd into the directory
run
npm install
start developing


Sections in this article
- MyCoin implementation in Live-Common
- Introduction
- Setup
- Structure
- Generating families-specific imports
- Building for Development
- Step by Step integration
- Testing
MyCoin implementation in Live-Common
Introduction
Ledger Live Common is the shared core library used by Ledger Live Desktop and Mobile, that also includes a CLI for testing purpose or for using Ledger Live features directly from a terminal (in a limited way).
This library is built upon a pretty standard ES6 + FlowType stack and relies on a bunch of ledgerjs packages, RxJS 6.x, bignumber.js and React + Redux for some front-end utilities and hooks.
It is designed to have very generic models and mechanics (for currencies, accounts, storage, synchronisation, events…) that also facilitates new coin integrations through flexibility.
All integrated coins are implemented in a src/families
dedicated folder which contains the specifics of a coin family - that can be shared by multiple crypto-assets that use the same implementation (i.e. Bitcoin-like coins share the same bitcoin
family).
Some of those families are implemented using the Ledger’s libcore which is a monolithic library built in C++ that we tend to replace by JS implementations.
This document only concerns new coin integrations using JavaScript - we will use an imaginary coin named MyCoin
as a walkthrough.
Setup
Requirements
- NodeJS LTS/Erbium (Node 12.x)
- Yarn 1.x (Classic)
- Python 2.7 or 3.5+
- A C/C++ toolchain (see node-gyp documentation)
Development tools (used or required)
- yalc for locally linking live-common with other projects
- eslint - ensure it’s working in your IDE (vscode plugin)
- prettier - through an eslint-plugin
- flowtype - ensure it works fine with your IDE (vscode plugin)
Hardware prerequisites
- A physical device (Ledger Nano S or Ledger Nano X)
- MyCoin app installed on device
Installation
- Clone project https://github.com/LedgerHQ/ledger-live-common
yarn install
will install all dependenciesyalc publish --push
will build and link live-common
CLI setup
cd cli/
yalc add @ledgerhq/live-common
yarn install
yarn link # will make ledger-live CLI available
You can test that your local Live Common and your device works correctly by executing a CLI command like:
ledger-live version # should print live-common version
ledger-live deviceInfo # should display information about connected device
Note: ensure yarn global bin
is in your $PATH. You can build automatically the CLI by running yarn watch
in a separate terminal to ensure ledger-live
bin is always up-to-date with your work.
If everything is fine, you are ready to start integrating your new coin MyCoin
!
Structure
Your whole implementation of MyCoin
must reside in a mycoin
folder in src/families/
, with the exception of some changes to apply in shared code.
Here is a typical family folder structure (JS integration):
./src/families/mycoin
├── bridge
│ └── js.js
├── hw-app-mycoin
│ └── MyCoin.js
├── api.js
├── hw-getAddress.js
├── errors.js
├── deviceTransactionConfig.js
├── account.js
├── transaction.js
├── serialization.js
├── cli-transaction.js
├── logic.js
├── cache.js
├── preload.js
├── react.js
├── specs.js
├── speculos-deviceActions.js
├── test-dataset.js
├── test-specifics.js
└── types.js
Note: You can refer to existing implementations to complete given examples, like Polkadot integration
In the section Below, the files are marked as Required, Recommended, or Optional accordingly.
Generating families-specific imports
All per-family imports are gathered in the src/generated
folder, for letting generic code to choose which implementation to use on a per-coin basis.
You should not update those files manually, but call a dedicated script:
./scripts/sync-families-dispatch.sh
NOTE: you may need to add mycoin
to the withoutNetworkInfo
in this script if you don’t use a NetworkInfo type.
As soon as you add a file in the family (that is part of the generated folder) you will need to execute this script before building. Otherwise, your implementation will not be known by the live-common library.
Building for Development
Do not forget to build before testing:
yalc publish --push
# if not yarn watch
cd cli
yarn build
Environment Variables
Ledger Live provides a lot of flexibility through ENV variables. You can export them, define them before calling cli or use a tool like direnv.
To list them all, you can execute:
ledger-live envs
The one you will use the most before releasing you integration is:
EXPERIMENTAL_CURRENCIES=mycoin
It will consider mycoin
as supported (you can also add it to the supported currencies in cli/src/live-common-setup-base.js
).
For clarity, we will omit this environment variable in this document.
If needed, you can add your own in src/env.js
(always try to add a MYCOIN_ prefix to avoid collisions):
// const envDefinitions = { ...
MYCOIN_API_ENDPOINT: {
def: "https://mycoin.coin.ledger.com",
parser: stringParser,
desc: "API for mycoin",
},
// }
Linking local packages
You may need to override some package with a local version (like @ledgerhq/cryptoassets
or @ledgerhq/hw-app-mycoin
). Use yalc add package-name
to do so.
Since your build would be used locally during developemnt, you may also want those dependencies to be local and included in the published local package.
Ensure the .yalc
directory is included then in the package.json:
package.json
:
"files": [
...,
".yalc"
],
WARNING: Do not commit changes due to the usage of yalc
as it may result in wrong behaviour.
Step by Step integration
MyCoin cryptoassets
We will consider that MyCoin is already in @ledgerhq/cryptoassets package. If not, see MyCoin in CryptoAssets.
Derive Address from device
If your app JS bindings (see See My Coin App JS Bindings) are not yet published in LedgerJS, you can put them in src/families/mycoin/hw-app-mycoin
.
First and easiest step is to get an address from the device for MyCoin, by creating the hw-getAddress.js
Resolver:
src/families/mycoin/hw-getAddress.js
:
// @flow
import type { Resolver } from "../../hw/getAddress/types";
import MyCoin from "./hw-app-mycoin/MyCoin";
const resolver: Resolver = async (transport, { path, verify }) => {
const myCoin = new MyCoin(transport);
const r = await myCoin.getAddress(path, verify);
return {
address: r.address,
publicKey: r.publicKey,
path,
};
};
export default resolver;
Test that you can derive an address:
ledger-live getAddress --currency mycoin --path "44'/8008'/0'/0/0" --derivationMode ""
Derivation
Ledger Live uses the BIP44 derivation mode by default (as derivationMode=""
), which is standard and most common way for HD wallet.
If MyCoin has a conventional derivation path (BIP44), Ledger Live should already be able to derive an address correctly.
If you need to use another derivation mode:
Make changes to src/derivation.js
:
- Add a new derivation mode with
overridesDerivation
:
// const modes = Object.freeze({
// ...
mycoinbip44h: { // Hardened BIP44 for MyCoin
overridesDerivation: "44'/8008'/<account>'/0'/<address>'",
},
// });
- add the mode to family in
legacyDerivations
:
// const legacyDerivations: $Shape<CryptoCurrencyConfig<DerivationMode[]>> = {
// ...
mycoin: ["mycoinbip44h"],
// };
- disable the default use of BIP44 in
disableBIP44
:
// const disableBIP44 = {
// ...
mycoin: true,
// };
See Derivation documentation for further details.
You can check that the derivationMode is correct by executing:
ledger-live derivation --currency mycoin
#> default: MyCoin 1: 44'/8008'/<account>'/<change>/<address>
Types
Generic types can be found in src/types/, with some documentation in source.
Semantics
Semantic: What is an Account?
An account represents a wallet where a user detains crypto assets for a given crypto currency.
Ledger Live model is essentially an array of Account
because many accounts can be created, even within a same crypto currency.
More technically, an account is a view of the blockchain in the context of a specific user. While blockchain tracks the series of transactions, assets motions that happen between addresses, an account essentializes that from the point of view of a user that owns a set of addresses and wants to view the associated funds they detain and be able to perform transactions with it.
Essentially what the user wants to see at the end is his balance, a historic graph, and a series of past operations that were performed. Moving from the blockchain to the concept of account is not necessarily trivial (in blockchains like Bitcoin, the concept of account does not exist – you don’t create or destroy, this concept is a view, a lense, that we abstract for the user).
Semantic: What is an Operation?
Note the wording “Operation” is used instead of “Transaction”. An account does not have transactions, it has operations.
The same abstraction applies as for Account on top of blockchain: an Operation is a view of a transaction that happened in the blockchain that concerns the contextual account. It’s always in the context of an account, in other words, an operation does not exist on its own.
Most of the time, a transaction yield of one operation. But in some blockchains (like Ethereum) one transaction that concerns the account associated addresses can result in 0 to N operations. A typical example is contract or token transfers (one transaction to move a token generate a token operation and a fee operation in the ethereum account). Another example that is possible in most blockchains is a “self” transaction yields two operations (sending out, sending in). But in fact, that’s semantic, we, Ledger, have put. We could also have allowed the concept of “SELF” operation.
Semantic: What is a Transaction?
In Ledger Live, the “Transaction” is the data model that is created and updated in order to build the final blob to be signed by the device, and then broadcasted to the blockchain.
It is highly specific to the blockchain protocol, and contains any data that will need to be serialized into the transmission format of the blockchain. It only lives for a short period of time in the application - during which the user is choosing the parameters of its transaction before it is signed and sent to the blockchain, after which it will be transformed into an Operation.
Note that this Operation will be considered “pending”, as it is an optimistic version of the real Operation that will appear in the account history, after being synchronized.
Account
In Live Common, there are currently 3 types of accounts:
Account
which is a top level account associated to aCryptoCurrency
- the “main” account.TokenAccount
which is a nested level account, inside an Account and that is associated to aTokenCurrency
.ChildAccount
which is a variant of TokenAccount but more in context of aCryptoCurrency
(typically used for Tezos)
They are aggregated as a single AccountLike
type, used across Ledger Live implementations.
We will focus only on the Account
type as we won’t cover the Token integration in this document.
All main accounts share a common ground:
id: string
: a unique account identifier we build up with many pieces of information. It’s generally composed of 5 parts split by a:
.seedIdentifier: string
: a unique way to identify a seed the account was associated with.xpub?: string
: the xpub if relevant. This information is redundant with theid
and might be eventually dropped.derivationMode: DerivationMode
identifies the derivation scheme that is used. See Derivation.index: number
: the index of the account among a givenderivationMode
.freshAddress: string
: the “next” public address where a user should receive funds. For account-based blockchains, it is the current public address.freshAddressPath: string
represents the derivation path where thefreshAddress
was taken from.freshAddresses: Address[]
: an array of “future” fresh addresses. It is a generalisation offreshAddress
andfreshAddressPath
.name: string
: name of the account that the user has set. It is not saved or restored from any place but is defined by the user and locally saved.balance: BigNumber
represent the total amount of assets that this account holds.spendableBalance: BigNumber
: represents the subset ofbalance
that can be spent. Most of the time it will be equal tobalance
but this can vary in some blockchains.blockHeight: number
: tracks the current blockchain block height.currency: CryptoCurrency
: the associated crypto currency of the Account. See MyCoin in CryptoAssetsunit: Unit
: the user defined preferred unit to view the account with. It’s initialized tocurrency.units[0]
.operationsCount: number
gives the total number of operations this account contains. This field exists because theoperations
array is not guaranteed to be complete.operations: Operation[]
: an array of operations sorted from the most recent to the oldest one. It might be partial, containing only the last N operations but can be paginated on.pendingOperations: Operation[]
: likeoperations
but only for optimistic updates of operations resulting from transactions that were just performed and not yet confirmed.lastSyncDate: Date
: the date of the last time a synchronisation was performed.subAccounts?: SubAccount[]
: an optional field that is defined for accounts that can contain children accounts. This is for instance used for tokens and Tezos originated accounts.balanceHistory?: BalanceHistoryMap
: a cache that contains the historical datapoints of the balance in different ranges of time. It might not be present in Account and in that case, a fallback JS implementation will be used to calculate this from the operations array.
But if needed by the blockchain, an account can also contain coin-specific resources related to a single account, like its “nonce” or additional balances (e.g. for staking), or anything that may be displayed or used in your implementation. It’s generally an additional field like myCoinResources
. See Family-specific types below.
For further details, see Account documentation and src/types/account.js
Operation
In short, transactions history in Ledger Live is a list of Operation
, that are confirmed, unconfirmed or pending (not yet fetched from explorer).
They all share the same model, with an extra
field that can store any additional data you may need to display:
id: string
: unique identifier (encoded asaccountId-hash-TYPE
)hash: string
: transaction hash from blockchaintype: OperationType
: see Operation Type belowvalue: BigNumber
: atomic value of the operation, as a positive value that affects the account balance (includes fees for OUT, not for IN, only fees for FEES…)fee: BigNumber
: atomic value of the operation fees (no matter which direction)senders: string[]
: list of senders addressesrecipients: string[]
: list of recipients addressesblockHeight?: number
: (optional) height of the block on the blockchainblockHash?: string
: (optional) hash of the block the operation is intransactionSequenceNumber?: number
: sequence number of the transaction in blockchains (aka “nonce”)accountId: string
: the Ledger Live account iddate: Date
: parsed transaction datehasFailed?: boolean
: does the transaction succeededsubOperations?: Operation[]
: (advanced) in context of accounts that can have tokens, an operation can contains itself operationsinternalOperations?: Operation[]
: (advanced) in context of accounts that have internal transactions that belong to a parent transactionextra: Object
: Extra crypto specific fields
If MyCoin has specific operation fields (like additionalField
we added for example), you will be able to display them later. They are not meant to be useful in any flow, only for UI.
If MyCoin uses a “nonce”, then transactionSequenceNumber
must be filled correctly, as it will be necessary for signing new transactions (and will interpreted to clear pending operations). Only outgoing transaction must have this value though. See Optimistic Operation.
See src/types/operation.js for better understanding of all fields.
Operation Type
As said above, an Operation
has a type
which is generic string typed as OperationType
, giving more or less the direction of the operation:
OUT
: A send / transfer amount transactionIN
: A received / incoming amount transactionFEES
: A transaction that only charges feesNONE
: A transaction that does not affect balanceREWARD
: A claimed reward (as an outgoing transaction with fees)REWARD_PAYOUT
: A received reward (as an incoming transaction)SLASH
: A staking slash (with slashed amount generally)
There are more types available, existing one will have predefined icons, translations and behaviours (i.e. getOperationAmountNumber()
in src/operation.js).
MyCoin could have also specific operation types, if you need to add a type that is not yet implemented, add them in src/types/operation.js
. You will later need to implement some specific code for the Ledger Live Desktop and Mobile to display them correctly.
Transaction
Transaction
will contains any data needed to create a transaction on the blockchain. It is created as soon as the user initiates a transaction flow on Ledger Live, and will be updated according to its inputs, like the amount or choosing a recipient.
It shares a common shape between all coins, to which we will add specific-fields:
amount: BigNumber
: amount parsed from user input - will be 0 if user use all amountrecipient: string
: the recipient of this amountuseAllAmount?: boolean
: indicated if user wants to transfer whole available balancesubAccountId?: ?string
: the child/token account id if relevantfamily: string
: the coin-family of the transaction (as a coin discriminator)
Some coins also have some fields generally respecting the same semantics, here are some provided as example:
mode: string
: type of transaction to be broadcasted ("send"
,"freeze"
,"delegate"
,"claimReward"
…)fees: BigNumber
: fees (provided by blockchain, or filled by user)memo: ?string
: memo if required by exchange- …
Although some fields are required, they can be emptied (recipent = “” and amount = 0) and ignored for transactions making no use of it.
You should add any fields that would be required by MyCoin to be correctly broadcasted - respecting as much as possible its protocol’s lexicon.
See existing implementations for inspiration: Polkadot types, Cosmos types
Family-specific types
You will be implementing the flow types that will be used in your integration, like the Transaction type or the additional data needed to be added to the Account shared type, but also any other types that you will need (remember to always type your functions with flow types).
src/families/mycoin/types.js
:
// @flow
import type { BigNumber } from "bignumber.js";
s;
import type {
TransactionCommon,
TransactionCommonRaw,
} from "../../types/transaction";
// for legacy
export type CoreStatics = {};
export type CoreAccountSpecifics = {};
export type CoreOperationSpecifics = {};
export type CoreCurrencySpecifics = {};
/**
* MyCoin account resources
*/
export type MyCoinResources = {|
nonce: number,
additionalBalance: BigNumber,
|};
/**
* MyCoin account resources from raw JSON
*/
export type MyCoinResourcesRaw = {|
nonce: number,
additionalBalance: string,
|};
/**
* MyCoin transaction
*/
export type Transaction = {|
...TransactionCommon,
mode: string,
family: "mycoin",
fees: ?BigNumber,
// add here all transaction-specific fields if you implement other modes than "send"
|};
/**
* MyCoin transaction from a raw JSON
*/
export type TransactionRaw = {|
...TransactionCommonRaw,
family: "mycoin",
mode: string,
fees: ?string,
// also the transaction fields as raw JSON data
|};
/**
* MyCoin currency data that will be preloaded.
* You can for instance add a list of validators for Proof-of-Stake blockchains,
* or any volatile data that could not be set as constants in the code (staking progress, fee estimation variables, etc.)
*/
export type MyCoinPreloadData = {|
somePreloadedData: Object,
|};
export const reflect = (_declare: *) => {};
Note: Core types should be exported for legacy compatibility with existing libcore integrations.
Since some of thoses types will be serialized when stored or cached, you may need to define serialize/deserialize functions for those:
src/families/mycoin/serialization.js
:
// @flow
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),
};
}
Because of Account being generic, you may need to add your specific resources to src/types/account.js
…
// ...
import type {
MyCoinResources,
MyCoinResourcesRaw,
} from "../families/mycoin/types";
// export type Account = {
// ...
// // On some blockchain, an account can have resources (gained, delegated, ...)
myCoinResources?: MyCoinResources,
// };
// export type AccountRaw = {
myCoinResources?: MyCoinResourcesRaw,
// };
…and handle the associated serialization in src/account/serialization.js
:
// ...
import {
toMyCoinResourcesRaw,
fromMyCoinResourcesRaw,
} from "../families/mycoin/serialization";
// ...
export { toMyCoinResourcesRaw, fromMyCoinResourcesRaw };
// ...
// export function fromAccountRaw(rawAccount: AccountRaw): Account {
// const {
// ...
myCoinResources,
// }
// ...
if (myCoinResources) {
res.myCoinResources = fromMyCoinResourcesRaw(myCoinResources);
}
// return res;
// }
// export function toAccountRaw({
// ...
myCoinResources,
// }: Account): AccountRaw {
// ...
if (myCoinResources) {
res.myCoinResources = toMyCoinResourcesRaw(myCoinResources);
}
// return res;
// }
Note: if your integration of MyCoin does not require coin-specific data in an account, you will not need to define MyCoinResources
.
Operation, Account and Transaction serialization
Since Operation
will be stored as JSON, you will need to implement specific serializers for the extra
field.
We also would like the Operation
and Account
to be displayed in CLI with their specifics, so you must provide formatters to display them.
src/families/mycoin/account.js
:
// @flow
import invariant from "invariant";
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;
invariant(myCoinResources, "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,
};
NOTE: formatOperationSpecifics()
and formatAccountSpecifics()
are used in the CLI to display account-specific fields and extras of the transaction history, useful for debugging.
The same idea applies also to the Transaction
type which needs to be serialized and formatted for CLI:
src/families/mycoin/transaction.js
:
// @flow
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 };
Wrap your API
Before this part, you will need an indexer/explorer to get a list of operations. You may also obtain from it, or from a SDK, the state of an account on the blockchain, such as balances, nonce (if your blockchain uses something similar), and any data relevant to show or fetch in Ledger Live.
For the example, we will assume that MyCoin provides a SDK that is able to do both getting the state and the account history.
The best way to implement your API in Live Common is to create a dedicated api
subfolder, that exports all functions that require calls to 3rd-party and map their responses to Ledger Live types.
./src/families/mycoin
└── api
├── index.js
├── sdk.js
└── sdk.types.js
NOTE: try to separate as much as possible your different APIs (if you use multiple providers) and use typings to ensure you map correctly API responses to Ledger Live types
You will likely need to export thoses functions, but implemention is up-to-developer:
src/families/mycoin/api/index.js
:
export {
getAccount,
getOperations,
getPreloadedData, // adjust with needs of preloaded data
getFees,
submit,
disconnect, // if using persisting connection
} from "./sdk";
Basically, in the next sections, getAccount
will be called to create an Account
with balances and any additional resources, and getOperations
will be called to fill the operations[]
of this account, with the whole history of operations that can be requested incrementally. Then getFees
before sending a transaction to let the user know of the network cost (estimated or effective), and submit
to broadcast its transaction after signing.
See Polkadot Coin Integration’s api for good inspiration.
API Example
Here is an example of a MyCoin API implementation using a fictive SDK that uses something like WebSocket to fetch data.
NOTE: We don’t recommend using WebSocket as it could have drawbacks and is more difficult to scale up and put behind a reverse proxy. If possible, use HTTPS requests as much as possible.
// @flow
import MyCoinApi from "my-coin-sdk";
import type { MyCoinTransaction } from "my-coin-sdk";
import { BigNumber } from "bignumber.js";
import type { Operation, OperationType } from "../../../types";
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;
/**
* Connects to MyCoin Api
*/
async function withApi(execute: AsyncApiFunction): Promise<any> {
// If client is instanciated already, ensure it is connected & ready
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 {
// Handle Error or Retry
await disconnect();
}
}
/**
* Disconnects MyCoin Api
*/
export const disconnect = async () => {
if (api) {
const disconnecting = api;
api = null;
await disconnecting.close();
}
};
/**
* Get account balances and nonce
*/
export const getAccount = async (addr: string) =>
withApi(async (api) => {
const { balance, additionalBalance } = await api.getBalances(addr);
const nonce = await api.getNonce(addr);
const blockHeight = await api.getBlockHeight();
return {
blockHeight,
balance: BigNumber(balance),
additionalBalance: BigNumber(additionalBalance),
nonce,
};
});
/**
* Returns true if account is the signer
*/
function isSender(transaction: MyCoinTransaction, addr: string): boolean {
return transaction.sender === addr;
}
/**
* Map transaction to an Operation Type
*/
function getOperationType(
transaction: MyCoinTransaction,
addr: string
): OperationType {
return isSender(transaction, addr) ? "OUT" : "IN";
}
/**
* Map transaction to a correct Operation Value (affecting account balance)
*/
function getOperationValue(
transaction: MyCoinTransaction,
addr: string
): BigNumber {
return isSender(transaction, addr)
? BigNumber(transaction.value).plus(transaction.fees)
: BigNumber(transaction.value);
}
/**
* Extract extra from transaction if any
*/
function getOperationExtra(transaction: MyCoinTransaction): Object {
return {
additionalField: transaction.additionalField,
};
}
/**
* Map the MyCoin history transaction to a Ledger Live Operation
*/
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,
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,
};
}
/**
* Fetch operation list
*/
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)
);
});
/**
* Obtain fees from blockchain
*/
export const getFees = async (unsigned: string): Promise<BigNumber> =>
withApi(async (api: typeof MyCoinApi) => {
const { fees } = await api.dryRun(unsigned);
return BigNumber(fees);
});
/**
* Broadcast blob to blockchain
*/
export const submit = async (blob: string) =>
withApi(async (api: typeof MyCoinApi) => {
const { hash, fees } = await api.submit(blob);
// Transaction hash is likely to be returned
return { hash, fees: BigNumber(fees) };
});
If you need to disconnect from your API after using it, update src/api/index.js
to add your api disconnect in the disconnectAll
function, it will avoid tests and CLI to hang.
JS Bridge
bridge/js.js
is the entry point of a coin integration. It must export two bridges:
- a CurrencyBridge
- an AccountBridge
Starting with a mock
TO BE WRITTEN
// @flow
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 };
Split your code
You can now start to implement the JS bridge for MyCoin. It may need some changes back and forth between the types, your api wrapper, and the differente files.
The skeleton of src/families/mycoin/bridge/js.js
should look something like this:
// @flow
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";
import {
createTransaction,
updateTransaction,
prepareTransaction,
} from "../js-transaction";
import getTransactionStatus from "../js-getTransactionStatus";
import estimateMaxSpendable from "../js-estimateMaxSpendable";
import signOperation from "../js-signOperation";
import broadcast from "../js-broadcast";
const receive = makeAccountBridgeReceive();
const currencyBridge: CurrencyBridge = {
getPreloadStrategy,
preload,
hydrate,
scanAccounts,
};
const accountBridge: AccountBridge<Transaction> = {
estimateMaxSpendable,
createTransaction,
updateTransaction,
getTransactionStatus,
prepareTransaction,
sync,
receive,
signOperation,
broadcast,
};
export default { currencyBridge, accountBridge };
Note: you could implement all the methods in a single file, but for better readability and maintainability, you should split your code into multiple files.
Account Bridge
AccountBridge offers a generic abstraction to synchronize accounts and perform transactions.
It is designed for the end user frontend interface and is agnostic of the way it runs, has multiple implementations and does not know how the data is even stored: in fact it’s just a set of stateless functions.
Receive
The receive
method allows to derivate address of an account with a Nano device but also display it on the device if verify is passed in.
As you may see in src/families/mycoin/bridge.js
, Live Common provides a helper to implement it easily with makeAccountBridgeReceive()
, and there is a very few reason to implement your own.
Synchronization
We usually group the scanAccounts
and sync
into the same file js-synchronisation.js
as they both use similar logic as a getAccountShape
function passed to helpers.
src/families/mycoin/js-synchronisation.js
:
//@flow
import type { Account } from "../../types";
import type { GetAccountShape } from "../../bridge/jsHelpers";
import { makeSync, makeScanAccounts, mergeOps } from "../../bridge/jsHelpers";
import { getAccount, getOperations } from "./api";
const getAccountShape: GetAccountShape = async (info) => {
const { id, address, initialAccount } = info;
const oldOperations = initialAccount?.operations || [];
// Needed for incremental synchronisation
const startAt = oldOperations.length
? (oldOperations[0].blockHeight || 0) + 1
: 0;
// get the current account balance state depending your api implementation
const { blockHeight, balance, additionalBalance, nonce } = await getAccount(
address
);
// Merge new operations with the previously synced ones
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);
The scanAccounts
function performs the derivation of addresses for a given currency
and deviceId
, and returns an Observable that will notify every Account
that it discovered.
With the makeScanAccounts
helper, you only have to pass a getAccountShape
function to execute the generic scan that Ledger Live use with the correct derivation modes for MyCoin, and it will determine when to stop (generally as soon as an empty account was found).
The sync
function performs one “Account synchronisation” which consists of refreshing all fields of a (previously created) Account from your api.
It is executed every 2 minutes if everything works as expected, but if it fails, a retry strategy will be executed with an increasing delay (to avoid burdening a failing API).
Under the hood of the makeSync
helper, the returned value is an Observable of an updater function (Account=>Account), which is a pattern having some advantages:
- it avoids race conditions
- the updater is called in a reducer, and allows to produce an immutable state by applying the update to the latest account instance (with reconciliation on Ledger Live Desktop)
- it’s an observable, so we can interrupt it when/if multiple updates occurs
In some cases, you might need to do a postSync
patch to add some update logic after sync (before the reconciliation that occurs on Ledger Live Desktop). If this postSync
function is complex, you should split this function in a src/families/mycoin/js-postSyncPatch.js
file.
Reconciliation
Currently, Ledger Live Desktop executes this bridge in a separate thread. Thus, the “avoid race condition” aspect of sync might not be respected since the UI renderer thread does not share the same objects. This may be improved in the future, but for updates to be reflected during sync, we implemented reconciliation in src/reconciliation.js, between the account that is in the renderer and the new account produced after sync.
Since we might have added some coin-specific data in Account
, we must also reconciliate it:
src/reconciliation
:
// import {
// ...
fromMyCoinResourcesRaw,
// } from "./account";
// ...
// export function patchAccount(
// account: Account,
// updatedRaw: AccountRaw
// ): Account {
// ...
if (
updatedRaw.myCoinResources &&
account.myCoinResources !== updatedRaw.myCoinResources
) {
next.myCoinResources = fromMyCoinResourcesRaw(
updatedRaw.myCoinResources
);
changed = true;
}
// if (!changed) return account; // nothing changed at all
//
// return next;
// }
Transactions
Transaction
objects are created from a default state (createTransaction
), that will then be updated according to the flow and inputs of the user.
Everytime the transaction is updated through a patch (updateTransaction
), its parameters will need to be validated to check if transaction can be signed and broadcasted (see Validating Transactions).
In some cases, this transaction will need to be prepared to correctly check status (prepareTransaction
), like fetching the network fees, transforming some parameters, or setting default values, …
src/families/mycoin/js-transaction.js
:
// @flow
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;
};
Validating Transactions
We absolutely want to avoid users to sign transactions that do not meet the blockchain’s requirements, and would be rejected - or worse - fail, risking them to loose funds.
Hence, we will validate every parameters that could lead to invalid transactions:
For instance we will check that:
- the recipient is not empty
- the recipient address is valid
- the recipient exists
- user have enough funds for the transaction
- user have enough funds to pay transaction fees
- the amount is strictly positive (avoiding zero sends)
- the minimum balance or existential deposit is respected if relevant
- …
This validation is done everytime the user updates the transaction (any input changes) to give himeimmediate and contextual feedback. The status object returned by the getTransactionStatus
follows this definition:
errors: { [string]: Error }
: potential error for each (user) field of the transactionwarnings: { [string]: Error }
: potential warning for each (user) field for a transactionestimatedFees: BigNumber
: estimated total fees the tx is going to cost (in the mainAccount currency)amount: BigNumber
: actual amount that the recipient will receive (in account currency)totalSpent: BigNumber
: total amount that the sender will spend (in account currency)recipientIsReadOnly?: boolean
: should the recipient be non editable
errors
and warnings
are key - value (error) objects that would have for each input (in the user perspective) the eventual error or notice that has been detected. To each key then it is expected that an input in Ledger Live Desktop and Mobile will exists to display the error.
src/families/mycoin/js-getTransactionStatus.js
:
// @flow
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;
Dealing with Logic and Errors
As seen in the previous section, getTransactionStatus
deals with errors in a Transaction, because of a wrong user input not meeting the coin logic.
Those are user-dependent errors handled in the UI for each input displayed, and are expected to happen from time to time - we are not throwing them.
But some errors can occur in a different context, and not be caused by user. Try as much as possible to handle all failing cases and throw coin-specific errors (if not generic error already exist), that you may define in an errors.js
file (for reusablity).
src/families/mycoin/errors.js
:
// @flow
import { createCustomErrorClass } from "@ledgerhq/errors";
/**
* MyCoin error thrown on a specifc check done on a transaction amount
*/
export const MyCoinSpecificError = createCustomErrorClass(
"MyCoinSpecificError"
);
Add all exports to src/errors.js
:
// ...
export * from "./families/mycoin/errors";
Also, to avoid repeating code and facilitate usage of checks and constants, gather all your coin-specific logic functions in a single file (calculations, getters, boolean checks…). This will also ease maintenance, for instance when the blockchain’s logic changes (constants or additional checks added).
src/families/mycoin/logic.js
:
// @flow
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;
};
Building and Signing transaction
The Transaction
object is not exactly the transaction in the shape of the blockchain’s protocol (which is generally serialized into a blob of bytes). So for convenience, you may implement a buildTransaction
method to serialized it using MyCoin SDK, that could be reused for instance for estimating fees through the API.
src/families/mycoin/js-buildTransaction.js
:
// @flow
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);
};
This buildTransaction
function would return an unsigned transaction blob that would be signed with the MyCoin App on device:
src/families/mycoin/js-signOperation.js
:
// @flow
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 { open, close } from "../../hw";
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> =>
Observable.create((o) => {
async function main() {
const transport = await open(deviceId);
try {
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,
},
});
} finally {
close(transport, deviceId);
}
}
main().then(
() => o.complete(),
(e) => o.error(e)
);
});
export default signOperation;
The signOperation
function returns an Observable that will notify its subscriber when the user grated the signature.
It must notify it with the signedOperation, with signature
(generally contains the whole blob to be broadcasted) and operation
.
This operation is an optimistic version of the Operation
that would be displayed in the account history as a “Pending Operation” if the broadcast succeed. This pending operation is important to give feedback to the user, but may also be required for calculate the nonce (if relevant).
Broadcast
Once the transaction is signed, it must be broadcasted to MyCoin network. This is pretty easy if you correctly wrapped your API.
src/families/mycoin/js-broadcast.js
// @flow
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;
This function must return the optimistic operation, patched with the hash
generally provided by the network.
Once the operation is synced from MyCoin API, the AccountBridge will remove this optimistic operation from the pendingOperations
.
NOTE: when there are some pending operations, synchronization will occur every minutes.
Estimate Max Spendable
The maximum spendable amount is the total balance in an account that is available to send in a transaction. This amount is specific to MyCoin, so you will need to provide this value depending on the transaction the user want to send.
See https://support.ledger.com/hc/en-us/articles/360012960679-Maximum-spendable-amount
src/families/mycoin/js-estimateMaxSpendable.js
// @flow
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;
Here, we only return the spendableBalance, but without the fees. Since Fee estimation can be used elsewhere (like in the prepareTransaction
), you can put it’s logic in a dedicated js-getFeesForTransaction.js
file. Here is an example for a fee fetched from network from an unsigned transaction (a bit like Polkadot), but you can also have specific calculation, with fee-per-byte value provided by the blockchain.
src/families/mycoin/js-getFeesForTransaction.js
:
// @flow
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;
Testing send with CLI
Before being able to test a send
operation with CLI you will need to bind arguments and infer a transaction from it. Since we defined a “mode” field in the transaction, this will be the only argument that will be necessary to test a send.
src/families/mycoin/cli-transaction.js
:
// @flow
import invariant from "invariant";
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 }) => {
invariant(transaction.family === "mycoin", "mycoin family");
if (account.type === "Account") {
invariant(account.myCoinResources, "unactivated account");
}
return {
...transaction,
family: "mycoin",
mode: opts.mode || "send",
};
});
}
export default {
options,
inferTransactions,
};
Of course if MyCoin has more complex transactions, you can add many arguments to CLI. You can also define you own cli commands for any specific data you would like to fetch. See Polkadot CLI commands.
Now you can try a getTransactionStatus
or a send
:
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":"134ad522346bee108fa42a512788494e","type":"OUT","blockHash":null,"blockHeight":null,"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"}
Currency Bridge
CurrencyBridge offers a generic abstraction (for all crypto currencies) to add accounts with a Ledger device.
It is responsible for scanning accounts for a crypto family, but also preloading any data needed for the integration to work.
Scanning accounts
As we have seen Synchronization, the scanAccounts
, which is part of the CurrencyBridge, share common logic with the sync function, that’s why we preferably put them in a js-synchronisation.js
file.
The makeScanAccounts
helper will automatically execute the default address derivation logic, but for some reason if you need to have a completely new way to scan account, you could then implement your own strategy.
Preload currency data (optional)
Before creating or using a currency bridge (e.g. scanning accounts, or every 2 minutes for synchronization), Ledger Live will try to preload some currency data (e.g. tokens, delegators,…) required for live-common feature to correctly work.
This preloaded data will be stored in a persisted cache for future use, for Ledger Live to still work if temporarily offline, and speed up startup, depending on getPreloadStrategy
(preloadMaxAge
determines the data expiration that will trigger a refresh).
This cache contains the JSON serialized response from preload
which is then hydrated through hydrate
(which needs deserialization), directly after preload, or after a boot.
Live-Common features will then be able to reuse those data anywhere (e.g. validating transactions) with getCurrentMyCoinPreloadData
, or by subscribing to getMyCoinPreloadDataUpdates
observable.
src/families/mycoin/preload.js
:
// @flow
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: mixed): 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: mixed) => {
const hydrated = fromHydratePreloadData(data);
log("mycoin/preload", `hydrated foo with ${hydrated.somePreloadedData.foo}`);
setMyCoinPreloadData(hydrated);
};
Read more on Currency Bridge documentation.
Cache and performance
It is important to keep in mind that all currencies work independently and that Live Common provides a common framework to synchronize accounts with a polling strategy, and that network connectivity is not always stable and optimal.
Hence, the more cryptocurrencies Ledger Live is using, the more requests and calculations are executed, which can take time.
To avoid making the same requests several times, we recommend using a local cache in your implementation (e.g. fees estimations, some currency data to preload…) in a src/families/mycoin/cache.js
file.
We have a src/cache.js
helper for creating Least-Recently-Used caches anywhere if needed.
See for example the Polkadot’s cache implementation.
Front-End helpers
Live Common is mainly dedicated to be used by Ledger Live front-ends (Desktop and Mobile), so it also contains utilities for react and displaying crypto-specific data.
Device transaction fields
When signing a transaction, the user is shown on his device all the parameters of this transaction through multiple screen, that he must check towards the value he entered, and compare with what Ledger Live is presenting.
The list of all displayed fields on device are provided by the getDeviceTransactionConfig
function, which must return all transaction fields for a given transaction.
src/families/mycoin/getDeviceTransactionConfig.js
:
// @flow
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 = [];
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;
IMPORTANT NOTE: Since a not-well-informed user could be tricked to sign transaction with wrong recipients, we never show the destination
fields in Ledger Live applications, in order for users to get used to always verify it externally.
React hooks (optional / advanced)
If you are adding specific features to Ledger Live (like staking), you may need to access data through React hooks, that could provide common logic reusable for React components.
You are then free to add them in a src/families/mycoin/react.js
file.
See examples like sorting and filtering validators, subscribing to preloaded data observable, or waiting for a transaction to be reflected in account, in the Polkadot React hooks,
Icon
Icons are usually maintained by Ledger’s design team, so you must first check that MyCoin icon is not already added in live-common, in src/data/icons/svg. It contains cleaned-up versions of Cryptocurrency Icons from cryptoicons.co, organized by ticker.
If you need to add your own, they must respect those requirements:
- Clean SVG with only
<path>
elements representing the crypto - Size and viewport must be
24x24
- Icon should be about
18x18
and centered / padded - flat-styled, and must respect crypto color scheme (filled)
- No background or decorative shape added
- No
<g>
ortransform
,style
attributes…
Name should be the coin’s ticker (e.g. MYC.svg
) and must not conflict with an existing coin or token.
When building live-common, a script automatically converts them to React and React Native components.
Testing
See Testing


Congress to have a database of civil and social groups
Rachanatmak Congress to work together with social groups
by Special CorrespondentA three-member Congress team led by TPCC general secretary Vinod Reddy explained the initiatives of the party in Telangana in reaching out to the civil and social groups as envisaged by the party’s new wing — Rachanatmak Congress.
Mr. Reddy said he had made a presentation at the Rachanatmak Congress meeting held in New Delhi on how the party had reached out to civil and social groups and drafting all sections into its fight against the TRS government.
He said the alliance of Congress and CPI and also new political parties like Telangana Jana Samiti during the Telangana Assembly elections was a pointer. During that process of strengthening a pressure group against the TRS government’s misrule, he said several NGOs and social organisations were brought under one platform.
The Rachanatmak Congress, he said will help the party strategise in new political situations and coordinate with civil and social groups in the country. A nation-wide database of these organisations will also be set up soon, Mr. Reddy said.


Sarah Shatto
Bio ··
My name is Sarah, and I am a Code201 student. I previously obtained an AAAS from Shoreline Community College, and focused my studies on Graphic Design and Photography.  I My biggest goal is to continually learn- I want to keep breaking the cieling on what I thought was possible for myself. For the last six years, I had worked at the Apple Store, and I am waiting on a new position with Nordstrom's commercial photo studio as an IT Technician.
Work Experience ··
Apple, Inc., Lynnwood, Wa. — Genius Administrator
IVOXY Consulting, Seattle, WA. — Graphic Designer
Silicon Mechanics, Bothell, WA. — Graphic Design Internship
Nordstrom, Seattle, Wa. — Technical Support Technician
Providing IT support for Seattle Product photo studio. Troubleshooting hardware and software issues in a timely and efficient manner. Hardware repairs, implementing upgrades, internal customer service.
Led the charge in opening new store and creating foundational systems for the ordering of parts, arranging of repair room, contact methods for customer interaction. Resource for Technical Support Staff, troubleshooting issues with repairs. Building strong relationships with team members and maintaining healthy feedback culture. Created visual graphics for new systems and policies as well as all store presentations. Created and facilitated training for the Genius Administrator role for all stores in the market to ensure consistent experience across stores.
<
Collaborated with the Director of Marketing to execute assets that maintained visual standards and brand identity. Created visual graphics to support the priorities of the marketing team for various events and campaigns.
Worked with the marketing team to create visual components for print and web use. Created content for promotional materials that maintained brand standards for promotional materials. Collaborated with the in house graphic designer to design web pages, copy writing and editing print layouts.
Education ··
-
Code Fellows, Seattle, Wa. — Certificate ·· APRIL 2020
-
Shoreline Community College, Seattle, Wa. — A.A. in Graphic Design ··2014-2018
Code 102 immersive covering HTML, CSS, and Javascript in an interactive Web environment.
Assosciates in Graphic Design. Web and Print Application, Design Theory. Focus in studio photography.
My top ten favorite foods··
- 1. Steak
- 2. Dark Chocolate
- 3. Street Tacos
- 4. Ramen
- 5. Bacon
- 6. Ice Cream
- 7. Deep Dish Pizza
- 8. Cheddar and Sour Cream Ruffles
- 9. Thai Food
- 10. Peanut Butter


Jacob Zuma files leave to appeal in graft case
by Staff ReporterJOHANNESBURG (AP) — Former South African president Jacob Zuma has filed notice to appeal a court ruling that cleared the way for him to be prosecuted for corruption.
Zuma’s lawyers are arguing in Pietermaritzburg High Court on Friday to be granted leave to appeal.
The court had dismissed his application to have the corruption case against him permanently halted.
Zuma has alleged his case was prejudiced by lengthy delays in bringing the matter to trial. He also has alleged political interference.
He denies charges of corruption, money laundering and racketeering related to the country’s controversial 1999 arms deal.
Zuma is accused of receiving bribes from French arms manufacturer Thales through his former financial adviser Schabir Shaik, who was convicted of fraud and corruption in 2005.


Bootstrap core CSS ?>
JavaScript remake of the classic game "Rock Raiders" showcasing usage of the Rygame HTML5 Port!
Assets not included! In order to play the game, you have to provide a reference below to the original game files.
This option only works, if hosted with proper CORS settings.


Resources
Catalog
The catalog is a list of all repositories stored in the registry.
Find Repositories
Returns a list of all existing repositories. A repository is a set of images with the same name and different tags.
HTTP Request
GET /v2/_catalog
Query Parameters
| Name | Type | Description |
|---|---|---|
| n | integer | Maximum number of results |
| last | string | Last result from previous response |
Responses
| Status | Type | Description |
|---|---|---|
| 200 | Repositories |
Registry
Find API version
HTTP Request
GET /v2/
Responses
| Status | Type | Description |
|---|---|---|
| 200 | ||
| 401 | Errors | Unauthorized |
| 404 | Endpoint does not implement registry v2 protocol |
Repository
Find Tags
HTTP Request
GET /v2/{repository}/tags/list
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
Responses
| Status | Type | Description |
|---|---|---|
| 200 | Tags |
Find Manifest
HTTP Request
GET /v2/{repository}/manifests/{reference}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| reference | Reference | Manifest reference |
Responses
| Status | Type | Description |
|---|---|---|
| 200 | Manifest |
Update Manifest
HTTP Request
PUT /v2/{repository}/manifests/{reference}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| reference | Reference | Manifest reference |
Delete Manifest
HTTP Request
DELETE /v2/{repository}/manifests/{reference}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| reference | Reference | Manifest reference |
Find Blob
HTTP Request
GET /v2/{repository}/blobs/{digest}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| digest | Digest |
Delete Blob
HTTP Request
DELETE /v2/{repository}/blobs/{digest}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| digest | Digest |
Responses
| Status | Type | Description |
|---|---|---|
| 202 |
Create Upload
HTTP Request
POST /v2/{repository}/blobs/uploads
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
Find Upload
HTTP Request
GET /v2/{repository}/blobs/uploads/{uuid}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| uuid | string |
Update Upload
HTTP Request
PUT /v2/{repository}/blobs/uploads/{uuid}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| uuid | string |
Delete Upload
HTTP Request
DELETE /v2/{repository}/blobs/uploads/{uuid}
Path Parameters
| Name | Type | Description |
|---|---|---|
| repository | RepositoryName | Repository name |
| uuid | string |
Types
Configuration
| Property name | Type | Description |
|---|---|---|
| mediaType* | string | |
| size* | integer | |
| digest* | Digest |
Digest
Error
The error codes encountered via the API are enumerated in the following table:
| Code | Message | Description |
|---|---|---|
BLOB_UNKNOWN |
blob unknown to registry | This error may be returned when a blob is unknown to the registry in a specified repository. This can be returned with a standard get or if a manifest references an unknown layer during upload. |
BLOB_UPLOAD_INVALID |
blob upload invalid | The blob upload encountered an error and can no longer proceed. |
BLOB_UPLOAD_UNKNOWN |
blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. |
DIGEST_INVALID |
provided digest did not match uploaded content | When a blob is uploaded, the registry will check that the content matches the digest provided by the client. The error may include a detail structure with the key "digest", including the invalid digest string. This error may also be returned when a manifest includes an invalid layer digest. |
MANIFEST_BLOB_UNKNOWN |
blob unknown to registry | This error may be returned when a manifest blob is unknown to the registry. |
MANIFEST_INVALID |
manifest invalid | During upload, manifests undergo several checks ensuring validity. If those checks fail, this error may be returned, unless a more specific error is included. The detail will contain information the failed validation. |
MANIFEST_UNKNOWN |
manifest unknown | This error is returned when the manifest, identified by name and tag is unknown to the repository. |
MANIFEST_UNVERIFIED |
manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned. |
NAME_INVALID |
invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. |
NAME_UNKNOWN |
repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. |
SIZE_INVALID |
provided length did not match content length | When a layer is uploaded, the provided size will be checked against the uploaded content. If they do not match, this error will be returned. |
TAG_INVALID |
manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. |
UNAUTHORIZED |
authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. |
DENIED |
requested access to the resource is denied | The access controller denied access for the operation on a resource. |
UNSUPPORTED |
The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. |
| Property name | Type | Description |
|---|---|---|
| code* | ErrorCode | |
| message* | string | |
| detail* | any |
ErrorCode
Errors
{
"errors":[
{
"code":"NAME_UNKNOWN",
"message":"repository not known to registry",
"detail":"i-do-not-exist"
}
]
}
List of errors.
| Property name | Type | Description |
|---|---|---|
| errors* | Error[] |
Layer
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"size":1605,
"digest":"sha256:c39b7f8739a4278307195461d3f7e9d4751f446c55822356a941d2a15a4008c4"
}
| Property name | Type | Description |
|---|---|---|
| mediaType* | string | |
| size* | integer | |
| digest* | string | |
| urls | string[] |
Manifest
{
"schemaVersion":2,
"mediaType":"application/vnd.docker.distribution.manifest.v2+json",
"config":{
"mediaType":"application/vnd.docker.container.image.v1+json",
"size":7023,
"digest":"sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
},
"layers":[
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"size":32654,
"digest":"sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
},
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"size":16724,
"digest":"sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
},
{
"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip",
"size":73109,
"digest":"sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
}
]
}
| Property name | Type | Description |
|---|---|---|
| schemaVersion* | integer | |
| mediaType* | string | |
| config* | Configuration | |
| layers* | Layer[] |
Reference
Name or digest
Repositories
List of repositories.
| Property name | Type | Description |
|---|---|---|
| repositories* | string[] |
RepositoryName
Tags
List of tags of a given repository
| Property name | Type | Description |
|---|---|---|
| name* | string | Repository name |
| tags* | string[] | List of tags |
Built-in Types
any
Any simple or complex type.
date-only
The "full-date" notation of RFC3339, namely yyyy-mm-dd. Does not support time or time zone-offset notation.
datetime
A timestamp in one of the following formats: if the format is omitted or set to rfc3339, uses the "date-time" notation of RFC3339; if format is set to rfc2616, uses the format defined in RFC2616.
datetime-only
Combined date-only and time-only with a separator of "T", namely yyyy-mm-ddThh:mm:ss[.ff...]. Does not support a time zone offset.
file
The file type can constrain the content to send through forms. When this type is used in the context of web forms it should be represented as a valid file upload in JSON format. File content should be a base64-encoded string.
integer
Any positive or negative integer JSON number in decimal representation.
number
Any integer or floating-point JSON number in decimal representation.
string
Any Unicode character string, encoded in UTF-8.
time-only
The "partial-time" notation of RFC3339, namely hh:mm:ss[.ff...]. Does not support date or time zone-offset notation.


| Rows: {{ac.rows}} |
| Columns: {{ac.cols}} |
For the animation range enter a location starting from the bottom left corner of the sprite. Index starts at 0. Enter as a range or comma separated. Eg: 2-10 or 0,1,3,12
Name: Range:
{{entry.animName}}: {{entry.range}}