SDK Cheat Sheet
Here you will find a list of common scenarios for using the SDK with different parameters
Initial Setup
Below you will find the initialization of some variables that will be used across different examples
SDK Initialization
Create a new instance of the SDK
import { LedgerSdk } from '@minka/ledger-sdk'
const sdk = new LedgerSdk({
// Handle of the ledger to use
ledger: 'my-ledger',
// Server to connect, without trailing slash
server: 'http://localhost:3000/v2',
// Optional, timeout in milliseconds. Defaults to 15000
timeout?: 15000,
// Optional, automatically verify server responses
verifyResponseProofs: true,
// Optional, Public key used to check responses coming from the server
signer: {
format: 'ed25519-raw', // Only format supported for now
public: '<ledger public key>'
},
// Optional, default values for auth composition
secure: {
iss: '<issuer of jwt>', // signer handle or public key
sub: '<subject for the generated jwt>', // signer:<handle>, bridge:<handle>, ...
aud: '<target audience of the jwt>', // ledger handle or public key
exp: 60, // Expiration time of tokens, in seconds
keyPair: { // Public-secret key pair used to sign tokens
format: 'ed25519-raw',
public: '<public key>',
secret: '<secret key>'
}
},
})
If verifyResponseProofs
is true, the signer
property is mandatory as it will be used to automatically check responses.
If secure
is provided, all requests made by the SDK will send a JWT token to authenticate
Access Rules
Let’s create a simple helper function to generate access rules that will grant all permissions to the provided public key, meaning that public key is the owner of the record.
const getOwnerAccessRules = (publicKey) => {
return [
{
action: 'any',
signer: {
public: publicKey
}
},
{
action: 'read',
bearer: {
$signer: {
public: publicKey
}
}
}
]
}
If you want more information regarding access rules, see also:
How to set up record access rules
Creating a Local Signer
In real scenarios we shouldn’t use a signer for everything, but in this case let’s create a Key Pair that we can use for everything
// Make sure this code is inside an async function so you can use await
const signer = sdk.signer
.init()
.data({ handle: 'local_signer' })
.keys({ format: 'ed25519-raw' })
const { data } = await signer.read()
// Extract only the data we need for signing
const keyPair = {
format: data.format,
public: data.public,
secret: data.secret
}
Creating a Remote Signer
Previously, we created a keyPair
locally, but in case we want to store it remotely we can self-sign it and send to the ledger. Note that secret
is optional and you don’t need to store it on the ledger.
const signerData = {
handle: 'remote_signer',
format: keyPair.format,
public: keyPair.public,
secret: keyPair.secret,
access: getOwnerAccessRules( keyPair.public )
}
const remoteSigner = sdk.signer
.init()
.data(signerData)
.code('password') // Optional, this will encrypt secret key
.hash()
.lock() // Will sign the record using the own key pair
// Send the request to the server, this operation is always asynchronous
await remoteSigner.send()
Fetching and decoding a Remote Signer
After storing a signer remotely, you can fetch it anywhere to sign some other records using it. If the signer secret was encrypted using the .code
method, now you will use .code
again to decrypty it.
// Note that 'remote_signer' is the handle previously set
const { signer } = await sdk.signer.read('remote_signer')
const plainSigner = await sdk.signer
.init()
.data(signer)
.code('password') // If we used a password to encrypt the server
.read({plain: true}) // use plain:true to decrypt secret using password
// Just as we did when creating our local signer, we can extract the data
// that we need to use for signing
const remoteKeyPair = {
format: plainSigner.data.format,
public: plainSigner.data.public,
secret: plainSigner.data.secret
}
Onboarding
Let’s follow the process to have everything in place before we can make our first intents. For that, we may need Symbols and Wallets
Creating a Symbol
First, let’s create an USD Symbol. The code is very similar to creating a remote signer, in fact the creation of most records is very similar.
/**
* Factor is used to represent the decimal currency
* amounts as integers. Decimal amounts are multiplied
* with this factor when storing to ledger.
*/
const symbolData = {
handle: 'usd', // Unique identifier
factor: 100 // 100 stands for 2 decimal places
access: getOwnerAccessRules( keyPair.public )
}
await sdk.symbol
.init()
.data(symbolData)
.hash()
.sign([{keyPair}])
.send()
Creating a wallet
Now we need wallets that will hold balances of those symbols.
await sdk.wallet
.init()
.data({
handle: 'tesla',
access: getOwnerAccessRules( keyPair.public )
})
.hash()
.sign([{keyPair}])
.send()
await sdk.wallet
.init()
.data({
handle: 'minka',
access: getOwnerAccessRules( keyPair.public )
})
.hash()
.sign([{keyPair}])
.send()
Retrieving a wallet and its balance
After we created our wallet, or any record, we can use the read
method to retrieve it. For wallets, we also have a balances
method to get the balances of an specified wallet
const { wallet } = await sdk.wallet.read('tesla')
// Output: Wallet data for `tesla`
const { balances } = await sdk.wallet.getBalances('tesla')
// Output: Empty array, no balances for now
Registering a bridge
Registering a bridge on the server means creating a bridge
record for it, similar to what we did before with symbols and wallets.
await sdk.bridge
.init()
.data({
handle: 'teslabank',
config: {
server: 'http://tesla.com/bank/v2' // URL of the bridge
},
secure: [
// Security rules to define auth mechanism between bridge and server
]
access: getOwnerAccessRules( keyPair.public )
})
.hash()
.sign([{keyPair}])
.send()
If you want more information regarding bridges, see also:
Bank integration tutorial (open banking)
Updating a wallet (adding the bridge)
Now that we have a bridge registered, we can update our existing wallet to use this bridge.
// Retrieve the current wallet from the server
const { wallet, response } = await sdk.wallet.read('tesla')
// Update the wallet by passing the new field
await sdk.wallet
.from(response.data)
.data({
bridge: 'teslabank'
})
.hash()
.sign([{keyPair}])
.send()
// We can also update the wallet by passing the entire wallet on our `data` method
await sdk.wallet
.from(response.data)
.data({
...wallet, // Spread the old wallet data
bridge: 'teslabank'
}, true) // Pass true as second argument (replace)
.hash()
.sign([{keyPair}])
.send()
Sending intents
Now we are ready to send intents between our wallets. Please have in mind that we are assuming that the server passed as property to config.server
in our teslabank
bridge is up and running, confirming all of our transactions.
Creating an intent
First, let’s create an issue intent to our tesla
wallet, so it holds some balance.
const claim = {
action: 'issue',
target: 'tesla',
symbol: 'usd',
amount: 10000 // This stands for 100.00$, because usd has a factor of 100
}
await sdk.intent
.init()
.data({
handle: sdk.handle.unique(), // will return a random unique handle
claims: [ claim ],
access: getOwnerAccessRules(keyPair.public)
})
.hash()
.sign([{keyPair}])
.send()
const { balances } = await sdk.wallet.getBalances('tesla')
// Output: [ { wallet: 'tesla', symbol: 'usd', amount: 10000 } ]
Signing an existing intent
In some cases we want to add signatures to intents that already exists, for example when validating them from bridges, it’s similar to an update but adding the signatures instead of changing the data.
const { response } = await sdk.intent.read('<intent-handle>')
await sdk.intent
.from(response.data)
.hash()
.sign([
{
keyPair,
custom: { // Optional custom data for the signature
status: 'prepared'
}
}
])
.send()
Verifying record’s proofs
By getting the property .proofs
of our SDK instance, we can get a proofs verifier client that allows us to verify the proofs of a record
const { response } = await sdk.intent.read('<intent-handle>')
const record = response.data
const verifier = sdk.proofs
// We can add assertions to our verification client
verifier.length(1) // At least 1 proofs present, all clients have this by default
.length(2, 3) // At least 2 proofs, but less than 3
// Expect at least one proof made with the ledger signer specified on sdk initialization
.ledger()
// Expect at least one proof that partially matches the given object
.expect({ custom:{ status: 'prepared' } })
// The verify method checks all assertions on the given record.
// It alsos verifies that all proofs are valid.
await verifier.verify(record)
await verifier.verify(record) // Assertions are reusable with other records
// Get a new, clean verification client with only `length(1)` assertion.
const otherVerifier = sdk.proofs
await otherVerifier.verify(record)
Retrieving error from record proofs (if any)
If our intent fails for some reason, a signature will be added to our intent’s proofs containing the error reason. We can extract that error using the error
method of the proof verifier client
const { response: { data: record } } = await sdk.intent.read('<intent-handle>')
const error = sdk.proofs.error(record)
// Output: Found LedgerSdkError in the last proof, if any.
General concepts
Record Create, Read and Update
Creating records
The rules for creating different types of record may vary a little, but as you could see they are pretty much the same. Sending a signed record to the server
// Get the client
const client = sdk.wallet // Can be wallet, signer, symbol, intent...
// Initialize the record
const record = client.init()
// Set the data
record.data({
handle: 'my-wallet'
})
// Create a hash for that data
record.hash()
// Sign the record using provided array of parameters
record.sign([
{
keyPair,
},
{
keyPair,
/**
* Custom data can be assigned to signatures. This
* will be included in the calculation of the result
* for this specific signature, and should be used in
* the future to validate it.
*/
custom: {
identifier: 123456
}
}
])
// Send that record to the backend
await record.send()
/**
* You could also add the entire record data directly into the .init
* method of the record. This mean you can pass the hash and proofs by
* yourself instead of using `hash` and `sign` methods respectively, or
* just pass the data instead of using the .data method
*/
await client
.init({
data: {
handle: 'my-wallet'
},
hash: '<valid-hash>',
meta: {
proofs: [
{'Valid signature'},
// More proofs
]
}
})
.send()
Reading
When reading a record using the read
method, its returned value will have a property which name will match the name of the record (wallet
or symbol
, for example) that contains the record’s data, we will also get hash
and meta
properties for that specific record.
// Get the client
const client = sdk.symbol
// Get data for symbol with handle `usd`
const symbolResponse = await client.read('usd')
const record = {
hash: symbolResponse.hash // Hash of the record
meta: symbolResponse.meta // Metadata of the record, including proofs
data: symbolResponse.symbol // Data of the record
}
/**
* When updating, reading or creating records using the SDK the returned value
* will include the property `response`, which is an instance of axios response.
* That means you can also get the entire record from the body of that response.
*/
const record = symbolResponse.response.data; // Includes hash, meta and data properties
Updating
Similar to using the init
method to initialize the entire record, we can use the from
record with the entire record data (Including meta
and hash
) to update a record. Have in mind that the handle must exists and the hash will be validated.
// Get the client
const client = sdk.symbol
// Get the current record
const record = (await client.read('usd')).response.data
// Update an specific field of the record
await client
.from(record)
.data(
{
factor: 10
}
)
.hash() // The hash of the record will change with new data
.sign([{keyPair}]) // The new hash needs to be signed
.send()
// Update the entire record
await client
.from(record)
.data(
{
...record.data, // Spread the entire data of the old record
factor: 10
},
true // Pass true (replace) as second argument
)
.hash()
.sign([{keyPair}])
.send()
Lists and pagination
When reading records using the list
method, its returned value will have a property which name will match the name of the record in plural (wallets
and symbols
for example) containing a list of the record’s data.
// Get the client
const client = sdk.wallet
// Get the first 20 wallets (Default)
const { wallets } = await client.list()
// Get first 10 wallets
const { wallets } = await client
.list({
page: {
index: 0,
limit: 10,
},
})
// Get next 10 wallets
const { wallets } = await client
.list({
page: {
index: 1,
limit: 10,
},
})
/**
* Please note that the result of `wallets` will contain only data of the record.
* If you want the entire record (Including both hash and meta properties), you
* may need to get the list directly from the `response` property.
*/
const listResponse = await client.list()
const walletRecords = listResponse.response.data;
As described in About Querying, you can also apply filters when listing, in order to narrow down your results. For example, to only get the wallets whose schema is bank
, you would:
// Get first 10 bank wallets
const { wallets } = await client
.list({
page: {
index: 0,
limit: 10,
},
'data.schema.$eq': 'bank'
})
Schemas
schema
is a special type of record which contains a set of rules and constraints used to validate the record that references it. Schemas can be created for a specific record type and if at least one schema exists for any record type, all records of that type that are created or updated must reference at least one valid schema. Record types that can be validated using schemas are signer
, symbol
, wallet
, bridge
, intent
, effect
and circle
Creating a schema
Creating a schema is just like creating any other record.
const { schema } = await sdk
.schema
.init()
.data(...)
.hash()
.sign([{keyPair}])
.send()
Referencing a schema
To reference a schema, you just need to pass the schema
property of the record's data with the handle of the desired schema.
const { wallet } = await sdk
.wallet
.init()
.data({
handle: 'my_wallet',
schema: '<schema_handle>',
})
.hash()
.sign([{keyPair}])
.send()
If you want more information regarding schemas, see also:
Summary of calls
When you see ...
, it’s a placeholder for the correct expected data, as documented earlier.
Signer
// Create a signer generating a random key pair
await sdk.signer
.init()
.keys({ // Use the .keys method to randomly generate key pair
format: 'ed25519-raw'
})
.code(...) // Optional
.hash()
.lock() // Sign using the record's key pair
.send()
// Create a signer providing an specific keypair
await sdk.signer
.init()
.data(...) // Use .data to pass the desired key pair
.code(...) // Optional
.hash()
.sign(...) // Sign with the provided key pairs
.send()
/**
* Note that .lock is similar to .sign, but you don't
* need to pass any parameter because .lock uses the
* key pair of the record itself.
*/
// Update a signer
await sdk.signer
.from(...)
.data(...)
.hash()
.sign(...) // Sign using any provided key pair
.lock() // Sign using it's own key pair
.send()
/**
* When using lock, you may need to previously call the
* .code method with the proper password to decrypt the
* secret key if it was previously encrypted.
*/
// Get data from an specific signer
await sdk.signer.read('<handle>')
// Get list of signers
await sdk.signer.list(...)
// Retrieve keys from a signer
await sdk.signer
.init()
.data({
handle: 'any-handle'
// If not public and secret is provided, they will be generated
// when calling the .keys method
})
.keys({ format: 'ed25519-raw' })
.code(...) // Optional
.read({plain:true})
Symbol
// Create a symbol
await sdk.symbol
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a symbol
await sdk.symbol
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific symbol
await sdk.symbol.read('<handle>')
// Get list of symbols
await sdk.symbol.list(...)
Wallet
// Create a wallet
await sdk.wallet
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a wallet
await sdk.wallet
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific wallet
await sdk.wallet.read('<handle>')
// Get list of wallets
await sdk.wallet.list(...)
// Get balances from an specific wallet
await sdk.wallet.getBalances('<handle>')
// Get domains from an specific wallet
await sdk.wallet.getDomains('<handle>')
// Get anchors from an specific wallet
await sdk.wallet.getAnchors('<handle>')
// Get anchors from an specific wallet via lookup. It enables to pass
// additional arguments
await sdk
.wallet
.with('<handle>')
.anchor.lookup()
.data(...)
.hash()
.sign(...)
.send()
See About Anchors and About Wallets for more details.
Bridge
// Create a bridge
await sdk.bridge
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a bridge
await sdk.bridge
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific bridge
await sdk.bridge.read('<handle>')
// Get list of bridges
await sdk.bridge.list(...)
Intent
// Create an intent
await sdk.intent
.init()
.data(...)
.hash()
.sign(...)
.send()
// Sign an intent
await sdk.intent
.from(...)
.hash()
.sign(...)
.send()
// Get data from an specific intent
await sdk.intent.read('<handle>')
// Get list of intents
await sdk.intent.list(...)
Effect
// Create an effect
await sdk.effect
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update an effect
await sdk.effect
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific effect
await sdk.effect.read('<handle>')
// Get list of effects
await sdk.effect.list(...)
Circle
// Create a circle
await sdk.circle
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a circle
await sdk.circle
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific circle
await sdk.circle.read('<handle>')
// Get list of circles
await sdk.circle.list(...)
// Assign a signer to a circle
await sdk.circle
.with('<circle handle>')
.signers
.init()
.data(...)
.hash()
.sign(...)
.send()
// List signers of a circle
await sdk.circle
.with('<circle handle>')
.signers
.list(...)
// Removing signers from a circle
await sdk.circle
.with('<circle handle>')
.signers
.with('<signer handle>')
.drop()
.hash()
.sign(...)
.send()
Schema
// Create a schema
await sdk.schema
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a schema
await sdk.schema
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific schema
await sdk.schema.read('<handle>')
// Get list of schemas
await sdk.schema.list(...)
Anchor
// Create an anchor
await sdk.anchor
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update an anchor
await sdk.anchor
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific anchor
await sdk.anchor.read('<handle>')
// Get list of anchors
await sdk.anchor.list(...)
Policy
// Create a policy
await sdk.policy
.init()
.data(...)
.hash()
.sign(...)
.send()
// Update a policy
await sdk.policy
.from(...)
.data(...)
.hash()
.sign(...)
.send()
// Get data from an specific policy
await sdk.policy.read('<handle>')
// Get list of policies
await sdk.policy.list(...)