Minka Ledger Docs
References

SDK Cheat Sheet

DateResponsibleChanges
January 16, 2023@Omar MonterreyInitial version
May 24, 2023@Omar MonterreyAdded Schemas section to general concepts. Added schemas and circles reference to Summary of calls.
May 25, 2023@Luis FidelisAdd commands for managing signers in a circle.
October 25, 2023@Luis Fidelis• Added commands for managing policies
• Added commands for managing anchors
• Added commands for querying anchors and domains from wallet client

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

About Authorization

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)

About Bridges

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 an schema

Creating an schema is just like creating any other record.

const { schema } = await sdk
	.schema
	.init()
	.data(...)
	.hash()
	.sign([{keyPair}])
	.send()

Referencing an schema

To reference an 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:

About Schemas

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>')
	.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(...)