4.0.22 • Published 1 year ago

@radixdlt/application v4.0.22

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
1 year ago

@radixdlt/application

High-level user-facing API for interacting with the Radix decentralized ledger.

Intro

import { Radix } from '@radixdlt/application'

const radix = Radix.create()
	.login('my strong password', loadKeystore)
	.connect(new URL('https://api.radixdlt.com/rpc'))
	.transferTokens({
		transferInput: {
			to: bob,
			amount: 1,
			tokenIdentifier: 'xrd_rb1qya85pwq',
		},
		userConfirmation: 'skip',
		message: { plaintext: 'Hey Bob, only we can read this.', encrypt: true }
	})
	.subscribe((txID) => console.log(`✅ TokenTransfer with txID ${txID.toString()} completed successfully.`)

Above code assumes you have a wallet. Looking for wallet creation?

💡 Please see README of @radixdlt/account package for a detailed documentation about getting started with a wallet.

RadixT

All interactions with the Radix ledger is exposed via the reactive interface Radix (of type RadixT) - built with RxJS 7 Beta (v13). Let's see an example of how everything "just works" thanks to using RxJS.

In the code block above we create a RadixT instance and provide it with a Hierarchical Deterministic (HD) wallet by loading a keystore using a function of type () => Promise<KeystoreT> (above named loadKeystore). After the keystore has been loaded, it will be decrypted using the provided password and create a wallet value (of type WalletT). Which the RadixT type will use internally to manage accounts and expose method for creating new ones. By default an initial account will be derived automatically1.

Lastly we subscribe to the reactive stream tokenBalances which will automatically fetch the token balances for the address of the active "account". See (Fetch Trigger)#fetchTrigger continuous update of balance ("polling").

Since the Radix Ledger supports multiple tokens for each address this will be a list of tokens, the amount, their symbol ("ticker") and name.

💡 Friendly reminder: the observables will not start emitting values until you have subscribed to them

💡 Friendly reminder: make sure to handle the returned value of the subscribe() function, by adding then to a Subscription object, otherwise behaviour is undefined, and you might experience all sorts of weird errors (e.g. memory leaks).

However, we can also interact with the Radix Core API without using any wallet, using the property api, like so:

const subs = new Subscription()

const radix = subs.add(Radix.create()
	.connect(new URL('https://api.radixdlt.com/rpc'))
	.setLogLevel(LogLevel.INFO)
	.ledger // accessing all RPC methods
	.nativeToken() // get token info about "XRD"
	.subscribe(nativeToken => {
		console.log(`💙 got nativeToken response: ${nativeToken}`)
	}))

/* In the near future... */
// "💙 got nativeToken response: {
//      "name": "Rads",
//      "resourceIdentifier": "/9SAU2m7yis9iE5u2L44poZ6rYf5JiTAN6GtiRnsBk6JnXoMoAdks/XRD",
//      "symbol": "XRD",
//      "description": "The native currency of the Radix network",
//      "granularity": "1",
//      "hasMutableSupply": false,
//      "currentSupply": "12000000000",
//      "url": "https://https://www.radixdlt.com/",
//      "tokenUrl": "https://avatars.githubusercontent.com/u/34097377?s=280&v=4",
// } "

In the code block above we did not provide any wallet, and we accessed the property ledger on which we called the method nativeToken() which observable stream we subsequently subscribe to. Lastly we handle the subscription.

The ledger property is separately documented in the end of this document

Reactive properties

In a GUI wallet you will most likely not use radix.ledger so much, but rather all the reactive properties (Observable variables) on the radix value directly.

Immortal state listeners

If any error where to be emitted on these reactive properties, they would complete (terminate), and you would miss out on any subsequently emitted value. We don't want that, why we've made sure that these never emit any value. All errors are redirected to the specific errors property, but more about that later. For now just remember that you will always get the latest and greatest data given the current active account from the radix interface.

Active address

We can subscribe to the active address, which will emit the formatted radix public address of the active account.

If we create and switch to a new account, we will see how both our active address, and our token balances would update automatically.

subs.add(radix.activeAddress.subscribe(
	(address) => console.log(`🙋🏽‍♀️ my address is: '${address.toString()}'`)
))

/* Instant */
// "🙋🏽‍♀️ my address is: '9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT'"

radix.deriveNextAccount({ alsoSwitchTo: true })

/* In the near future... */
// "🙋🏽‍♀️ my address is: '9S8PWQF9smUics1sZEo7CrYgKgCkcopvt9HfWJMTrtPyV2rg7RAG'"

// "💎 My token balances:
// [ 
//      237.0 'xwETH' ("Radix-Wrapped Ether")
// ]"

We subscribe to activeAddress which will automatically update with the address of our active "account". We will instantly see our initial account having address ending with "6RT" logged, since we already have an active account. Moments later we create a new account and also switched to it. Since we have subscribed to both tokenBalances and activeAddres, this will emit the token balances of the new account as well as the address of the new account.

⚠️ activeAddress will not emit any address until you have called connected to a node, because a network identifier fetched from the node is required to format an address.

Account listing

We can subscribe to all our accounts and list them using the observable property accounts, like so:

subs.add(radix.accounts.subscribe(
	(accounts) => console.log(`[🙋🏾‍♀️, 🙋🏼‍♀️] my accounts are: ${accounts.toString()}`)
))

/* Instant */
// "[🙋🏾‍♀️, 🙋🏼‍♀️] my accounts are: [
//      {
//          hdPath: "m/44'/536'/0'/0/0'"
//      },
//      {
//          hdPath: "m/44'/536'/0'/0/1'"
//      },
// ]

Well, that is not helpful! What are those? An account (of type AccountT) itself is not so user-friendly or beautiful to look at, it holds a reference to the derivation path used to derive it, and is in itself mostly a collection of functions. So printing them our in a console like this is not so helpful. However, when building a GUI wallet, we can display a clickable dropdown list of accounts and when a user selects an account we should switch to it. You can read about account switching further down.

Active account

You can also subscribe to just the single active account

subs.add(radix.activeAccount.subscribe(
	(account) => console.log(`🙋🏾‍♀️ my active account: ${account.toString()}`)
))

/* Instant */
// "🙋🏼‍ my active account: {
//  hdPath: "m/44'/536'/0'/0/0'"
// },

The activeAccount is probably not so useful, better to use the activeAddress and accounts properties

Token Balances

In the intro we subscribed to the tokenBalances. This will get updated automatically when you switch account.

subs.add(radix
	.tokenBalances
	.subscribe(({ tokenBalances }) => {
		console.log('💎 My token balances:')
		console.log(
			tokenBalances.map(
				balance => 
				`
				${balance.amount.toString()}
				${balance.token.toString()}
				`
			)
		)
	}
))

/* In the near future... */
// 💎 My token balances:
// [ 
//      5.37 rwBTC
// ]

// Later
radix
	.deriveNextAccount({ alsoSwitchTo: true })

/* In the near future... */
// 💎 My token balances:
// [ 
//      8541.37 rwETH
// ]

See (Fetch Trigger)#fetchTrigger for a way either scheduling fetching of token balances at a regular interval ("polling"), or triggering a single fetch when user presses a "Fetch Now" button in GUI.

Errors sink

Since RxJS observable finishes on error, and would stop emitting values after an error, we have made sure all errors on reactive properties, and only reactive properties (i.e. not method calls on ledger), are caught and redirected to the errors property (of type `Observable). Meaning that all reactive properties you can listen for values on are immortal.

💡 Any observable returned from a method call on ledger will emit errors, and only on that observable. The errors sink will NOT be aware of them.

Apart from logging (controlled with the setLogLevel method as seen in intro and documented below) it is probably a good idea to listen to errors and handle them appropriately. To be clear, you probably should act upon these errors, either you (as a GUI wallet developer) or prompt the user to take appropriate action(s).

radix.errors.subscribe(
	(errorNotification) => { 
		console.log(`☣️ error ${error.toString()}`)
		// Maybe not only log, but also act upon...	
	},
)

// "☣️ error { 'tag': 'node', msg: 'Invalid SSL certificate' }"
// "☣️ error { 'tag': 'wallet', msg: 'Failed to decrypt wallet' }"

The radix.errors reactive property is in itself immortal and will never error out, so do not add a subscriber to the error event, but rather the next events.

Error "categories"

The errors property emits three different category of errors, each error is tagged with a 'category', each can be regarded as a separate error channel/stream, and you can choose to split it into separate channels if you'd like.

import { Observable } from 'rxjs'
import { ErrorNotification, WalletError, ErrorNotification } from '@radixdlt/application'

const splitErrorNotificationsOnCategory = (category: ErrorCategory): Observable<ErrorNotificationT> => radix.errors.pipe(
	filter((errorNotification) => errorNotification.category === category),
)

const walletErrors = splitErrorNotificationsOnCategory(ErrorCategory.WALLET)

walletErrors.subscribe(
	(errorNotification) => {
		if (errorNotification.cause === WalletErrorCause.LOAD_KEYSTORE_FAILED) {
			console.log(`⚠️ failed to load keystore: '${errorNotification.message}'`)
			// Aslo display error message in GUI.
		}
	}
)

You can access the underlying error cause and even message with more details.

Methods

Local methods

None of these methods will result in any RPC call to the Radix Core API. All methods perform local computation only.

logLevel

Sets the log level of the internal logger of this SDK. We use loglevel. By default, only error and warning logs will visible to you. Lower the log level to see more information.

radix.logLevel('error')

The log levels available are trace, debug, error, warn, info, and silent.

Account derivation

You can create new accounts with deriveNextAccount(), which takes an optional alsoSwitchTo argument, which changes the current active account.

If you build a GUI wallet you probably want to locally save either a list of the derived accounts, i.e. their hdpaths or you might want to save the account with the highest value (the index of the last one), so that you can restore them upon app start.

For your convenience we provide you with a specific method for this

restoreAccountsUpToIndex

You can "restore" all accounts up to some last known index. This does not switch the active account. If you passed in the value 0 nothing will happen.

const localStore = localPersistentStoreAt('some/local/path') // or similar
const lastAccountIndex: number = localStore.loadLastAccountIndex() //  or similar

subs.add(radix.accounts.subscribe(
	(accounts) => console.log(`🙋🏾‍[] I have #${accounts.length} accounts`)
))

subs.add(radix
	.restoreAccountsUpToIndex(lastAccountIndex)
	.subscribe({
		complete: () => console.log(`✅ Finished restoring accounts`)
	}))


/* Later */
// "🙋🏾‍♀️[] I have #10 accounts"
// "✅ Finished restoring accounts"

Account switching

radix
	.switchAccount('first')
	.switchAccount({ toIndex: 1 })
	.switchAccount({ toIndex: 0 })
	.switchAccount('last')

/* Instant */
// "🙋🏽‍♀️ my address is: '9S8k...V6RT'"
// "🙋🏽‍♀️ my address is: '9S8P...7RAG'"
// "🙋🏽‍♀️ my address is: '9S8k...V6RT'"
// "🙋🏽‍♀️ my address is: '9S8P...7RAG'"

A GUI wallet would probably want to send in the selected account (of type AccountT) rather than use 'first' | 'last' | { toIndex: number } though. Which looks like this:

const selectedAccount: AccountT = accountListInGUI.selectedItem() // or similar
radix.switchAccount({ toAccount: selectedAccount })

/* Instant */
// "🙋🏽‍♀️ my address is: <THE_ADDRESS_OF_THE_SELECTED_ACCOUNT>"

TODO: 👀 we might want to make it possible to give each account a human-readable name, or that might be something a GUI wallet should be responsible for.

revealMnemonic

You can call revealMnemonic to get the mnemonic of the wallet you logged in with.

Token balance fetch trigger

You can specify a fetch trigger (polling):

import { timer } from 'rxjs'

radix
	.withTokenBalanceFetchTrigger(
		interval(3 * 60 * 1_000), // every third minute
	)

The above code will make sure you automatically perform a fetch of token balances every third minute.

Staking fetch trigger

Use withStakingFetchTrigger to specify a fetch trigger for unstakes/stakes. See balance fetch trigger for details.

Decrypt

You can decrypt encrypted messages using the private key of the active account in transactions like so:

subs.add(radix
	.decryptTransaction(transaction) // e.g. from tx history.
	.subscribe({
		next: (decrypted) => { console.log(`✅🔓 successfully decrypted message: ${decrypted.message.toString()}`) },
		error: (failure) => { console.log(`❌🔐 failed to decrypt message, wrong account? ${failure.toString()}`) },
	}))

Methods resulting in RPC calls

Transaction history

A transaction is not a token transfer, however, a token transfer might be one action amongst many in a transaction.

Transaction history might be long, and is for that sake paginated. So RadixT needs some kind of "cursor" together with a size, telling it where and how many transactions to fetch from the Radix Distributed Ledger.

subs.add(radix.transactionHistory({
	size: 3,
}).subscribe(
	(txs) => console.log(`📒⏱ transaction history: ${txs.toString()} ⏱📒`)
))

/* In the near future... */
// "📒⏱ transaction history: 
//	{
//		"cursor": "FadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBee",
//		"transactions": [
//			{
//				"id": "DeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBeefDeadBeef",
//				"transactionType": "incoming",
//				"sentAt": "2021-03-14",
//				"fee": "0.0123",
//				"message": {
//					"msg": "51358bd242d0436b738dad123ebf1d8b2103ca9978dbb11cb9764e0bcae41504b4521f0290ac0f33fa659528549//	d9ce84d230000003096dc6785ea0dec1ac1ae15374e327635115407f9ae268aad8b4b6ebae1afefbc83c5792de6fc3550d3//	e0383918d182e87876c9c0e3b5ca0c960fd95b4bd18421ead2aaf472012e7cfbfd7b314cbae588",
//					"encryptionScheme": "ECIES_DH_ADD_AES_GCM_V0"
//				},
//				"actions": [
//					{
//						"type": "TokenTransfer",
//						"from": "9SBRrNSxu6zacM8qyuUpDh4gNqou8QX6QEu53LKVsT4FXjvD77ou",
//						"to": "9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT",
//						"amount": "1337",
//						"resourceIdentifier": "/9SAU2m7yis9iE5u2L44poZ6rYf5JiTAN6GtiRnsBk6JnXoMoAdks/XRD"
//					}
//				]
//			},
//			{
//				"id": "ADeadBeeADeadBeeADeadBeeADeadBeeADeadBeeADeadBeeADeadBeeADeadBee",
//				"transactionType": "outgoing",
//				"sentAt": "2021-03-09",
//				"fee": "0.0095",
//				"actions": [
//					{
//						"type": "TokenTransfer",
//						"from": "9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT",
//						"to": "9S9tQA7v1jSEUTvLk3hTp9fTmWNsA1ppJ3D6dHLxoqnPcYayAmQf",
//						"amount": "1.25",
//						"resourceIdentifier": "/9SAU2m7yis9iE5u2L44poZ6rYf5JiTAN6GtiRnsBk6JnXoMoAdks/GOLD",
//					}
//				]
//			},
//			{
//				"id": "FadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBeeFadedBee",
//				"transactionType": "outgoing",
//				"sentAt": "2021-01-27",
//				"fee": "0.0087",
//				"actions": [
//					{
//						"type": "Stake",
//						"from": "9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT",
//						"toDelegate": "9S81XtkW3H9XZrmnzWqYSuTFPhWXdRnnpL3XXk7h5XxAM6zMdH7k",
//						"amount": "250",
//						"resourceIdentifier": "/9SAU2m7yis9iE5u2L44poZ6rYf5JiTAN6GtiRnsBk6JnXoMoAdks/XRD",
//					}
//				]
//			},
//		]
//	}
// ⏱📒
// "

Wow 😅, that's a mouthful... Let's break it down. We call subscribe to the observable returned by the method call to transactionHistory(...) and after a short delay log the received object. It contains some "cursor", being a pointer to the last transaction, we can use this for subsequent pagination pages. We also see an array of "transactions". Each transaction has:
1. An identifier.
2. A type (INCOMING, OUTGOING, FROM_ME_TO_ME or UNRELATED). 3. A date.
4. A token fee (paid in the native token ("Rad"/XRD)).
5. An optional, encrypted, message. 6. A list of actions, more about these below.

Above we saw an example fetching the 3 earliest transactions for an address.

We are unable to read the message attached to the first transaction, since it is encrypted. Messages are not decrypted automatically when received, you have to manual ask for a message to be decrypted, reade more about decryption here.

You ought to keep track of the returned cursor value in the transactionHistory response, since you can use that you query the next "page", like so:

import { Option, none } from 'prelude-ts'
import { TransactionIdentifierT } from '@radixdlt/application'
import { Subject } from 'rxjs'

const cursor: Option<TransactionIdentifierT> = none()
const fetchTXTrigger = new Subject<number>()

subs.add(fetchTXTrigger.pipe(
	mergeMap((pageSize) => {
		radix.transactionHistory({
			size: pageSize,
			cursor: cursor.getOrNull()
		})
	})
).subscribe(
	(txs) => { 
		cursor = Option.of(txs.cursor)
		console.log(`📒📃 got #${txs.size} transactions`)
	}
))

fetchTXTrigger.next(20) // fetch tx 0-19
fetchTXTrigger.next(20) // fetch tx 20-33

/* In the near future... */
// 📒📃 got #20 transactions
// 📒📃 got #14 transactions

In the code block above we use cursor to fetch two different "pages" of the transaction history, but this account only had 34 transactions, so the second page only contained 14 entries.

We use a Subject (RxJS) to trigger the multiple calls to transactionHistory, in combination with mergeMap ("flatMap) to transform the observable from number => TransactionHistory . An important thing to note is that we update the cursor upon receiving each new "page".

See (Fetch Trigger)#fetchTrigger for a way either scheduling fetching of transaction history at a regular interval ("polling"), or triggering a single fetch when user presses a "Fetch Now" button in GUI.

Actions

TokenTransfer

A transfer of some tokens, of a specific amount. This is probably the most relevant action.

StakeTokens

Staking of native tokens.

UnstakeTokens

Unstaking of staked native tokens.

Other

Two differnt kinds of actions fall in under this category:
1. Advanced actions: Well known actions that user cannot perform from the GUI wallet (MintTokens action amongst others). 2. Unknown actions: Ledger instructions that are not on canonical form (or otherwise unknown)

Make Transaction

Token transfer

Here we show how to transfer tokens, which is one of potentially several actions, making up a transaction.

import { Radix } from '@radixdlt/application'

const radix = Radix.create()
	.login('my strong password', loadKeystore)
	.connect(new URL('https://api.radixdlt.com'))

subs.add(radix.subscribe(
	.transferTokens(
		{
			transferInput: {
				to: bob,
				amount: 1,
				tokenIdentifier:
					'xrd_rr1qfumuen7l8wthtz45p3ftn58pvrs9xlumvkuu2xet8egzkcklqtesv2yq5l',
			},
			message: { plaintext: 'Hey Bob, only we can read this.', encrypt: true }
			userConfirmation: 'skip'
		}
	)
	.subscribe((txID) => console.log(`✅ Stake with txID ${txID.toString()} completed successfully.`))

Stake tokens

import { Radix } from '@radixdlt/application'

const radix = Radix.create()
	.login('my strong password', loadKeystore)
	.connect(new URL('https://api.radixdlt.com'))

subs.add(radix
	.stakeTokens(
		{
			stakeInput: {
				amount: 1,
				validator:
					'9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT',
			},
			userConfirmation: 'skip',
			pollTXStatusTrigger: pollTXStatusTrigger,
		}
	)
	.subscribe((txID) => console.log(`✅ Unstake with txID ${txID.toString()} completed successfully.`))

Unstake tokens

import { Radix } from '@radixdlt/application'

const radix = Radix.create()
	.login('my strong password', loadKeystore)
	.connect(new URL('https://api.radixdlt.com'))

subs.add(radix
	.unstakeTokens(
		{
			unstakeInput: {
				amount: 1,
				validator:
					'9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT',
			},
			userConfirmation: 'skip',
			pollTXStatusTrigger: pollTXStatusTrigger,
		}
	)
	.subscribe((txID) => console.log(`✅ TokenTransfer with txID ${txID.toString()} completed successfully.`))

The flow of making a transaction is the same, disregarding the contents of it, i.e. if you only make a single token transfer action, or a single stake tokens action, the flow remains the same.

Flow

  1. 🙋🏾‍♀️ userinputs transaction details (recipient, amount, token etc) and passes inputs to library.
  2. 💻 wallettransforms unsafe inputs into validated TransactionIntent.
  3. 🛠 libraryrequests Radix Core API to build transaction from intent and returns built transaction with human-readable fee to wallet.
  4. 🛠 librarysigns transaction
  5. 🛠 librarysubmits signed transaction to Radix Core API which promtly returns initial OK/ERR response, wallet handles this initial response. Response contains txID.
  6. OPTIONAL 💻 walletdisplays transaction fee and txID and waits for user to confirm transaction with PIN code.
  7. 🛠 libraryfinalizes signed transaction with txID to Radix Core API which promtly returns initial OK/ERR response, wallet handles this initial response.
  8. 💻 walletpolls status of transaction (using txID from step 5), using appropriate library api, and informs user of final CONFIRMED/REJECTED result.
  9. 🙋🏾‍♀️ useracts on any failures, e.g. presses "Retry"-button, if prompted with one because of network connection issues during step 7.

TransactionIntentBuilder

Unsafe user input

Let us transfer some tokens! All methods accept specific types such as AddressT for recipient address, AmountT for token amounts and ResourceIdentierT for token identifier (which you have access to via tokenBalances, nativeToken() and transactionHistory()).

💡 Amount of tokens to send must be a multiple of the token's granularity

You can read out the granularity (of type AmountT) from the token info, by using radix.ledger.tokenInfo(tokenResourceIdentifier).

For convenience you can pass in unsafe types, such as string as input to all actions, below we create a transaction intent with a single transferTokens action.

import { TransactionIntentBuilder } from '@radixdlt/application'

subs.add(TransactionIntentBuilder.create()
	.transferTokens({
		to: '9SBZ9kzpXKAQ9oHHZngahVUQrLwU6DssiPbtCj5Qb6cxqxPC6stb',
		amount: '12.57',
		tokenIdentifier: '/9SAU2m7yis9iE5u2L44poZ6rYf5JiTAN6GtiRnsBk6JnXoMoAdks/XRD'
	})
	.message('Thx for lunch Bob, let me pay for my salad.')
	.build({
		encryptMessageIfAnyWithAccount: activeAccount$, // Observable<AccountT>
	})
	.subscribe(
		(transactionIntent) => {
			console.log(`🎉 transactionIntent: ${transactionIntent.toString()}`)
		},
		(error) => {
			console.log(`🤷‍♂️ Failed to create transaction intent: ${error}`)
		},
	))

Alternatively you can transform input to save types eagerly, display relevant info for validation errors, and then pass these safe types to the TransactionIntent.

import { Amount } from '@radixdlt/primitives'
import { Address } from '@radixdlt/account'
import { Transaction } from '@radixdlt/application'

const recipientAddressString = recipientTextField.value() // or similar
const recipientAddressResult = Address.fromBase58String(recipientAddressString)
if (recipientAddressResult.isErr()) {
	console.log(`Invalid addres string, error: ${recipientAddressResult.error.message}`)
}
const recipientAddress: AddressT = recipientAddressResult.value

const fooToken: ResourceIdentifierT = selectedToken.id // or similar, read from `tokenBalances`.
subs.add(radix.ledger
	.tokenInfo(fooToken)
	.subscribe((token) => {
		console.log(`🔶🟠🔸 granularity of token ${token.name} : ${token.granularity.toString()}, any transfer of this token MUST be a multiple of this amount.`)
	}))

// Later when we know granularity of token.

const amountString = amountTextField.value() // or similar
const amountResult = Amount.fromUnsafe(amountString)
if (amountResult.isErr()) {
	console.log(`Invalid amount string, did you input a number?`)
}
const unsafeAmount: AmmountT = amountResult.value

if (!unsafeAmount.isMultipleOf(granularity)) {
	console.log(`⚠️ requested amount to send is not a mulltiple of token granularity, will be unable to send`)
	// 💡 also inform user in GUI
	// Abort token sending
}

// ☑️ Amount is checked against token granularity, safe to send.
const amount = unsafeAmount

subs.add(TransactionIntentBuilder.create()
	.transferTokens({
		to: recipientAddress, // safe type `AddressT`
		amount: amount, // safe type `AmounT`
		token: fooToken // safe type `ResourceIdentifierT`
	})
	.message(`Thx for lunch Bob, here's for my salad.`)
	.build(...)
	.subscribe(...))

Example

Here follows an axample of how we can make a transaction using, buildTransactionFromIntent and submitSignedTransaction.

We use the transactionIntent we built with TransactionIntentBuilder earlier.

Build TX

⚠️ Not yet implemented, subject to change.

const askUserToConfirmTransactionSubject = new Subject<UnsignedTransaction>()
const userDidConfirmTransactionSubject = new Subject<UnsignedTransaction>()
const pendingTransactionsSubject = new Subject<SignedTransaction>()

subs.add(radix
	.buildTransactionFromIntent({ 
		intent: transactionIntent, // from earlier
	})
	.pipe(
		tap((unsignedTxForUserToConfirm) => {
			const txFee = unsignedTxForUserToConfirm.fee
			console.log(`💵 tx fee: ${txFee.toString()}`)
			askUserToConfirmTransactionSubject.next(unsignedTxForUserToConfirm)
		})
	)
	.subscribe({ // Don't forget to subscribe
		error: (e) => {
			// Handle build tx errors
			if (isBuildTransactionError(e)) {
				switch (e) {
					case BuildTransactionError.INSUFFICIENT_FUNDS:
						console.log(`Insufficient funds`)
						// Display error to user
						return
					case BuildTransactionError.AMOUNT_NOT_MULTIPLE_OF_GRANULARITY:
						console.log(`Amount not multiple of granularity`)
						// Display error to user
						return
				}
			} else {
				console.log(`Unknown error building tx`)
			}
		},
	}))

Confirm TX

Require userDidConfirmTransactionSubject to emit value, either automatically done, or requiring manual input from user.

Here follows some pseudocode for what to do in GUI wallet.

Either automatically confirm tx

subs.add(askUserToConfirmTransactionSubject
	.subscribe((tx) => userDidConfirmTransactionSubject.next(tx)))

Or require manual confirmation, we could protect tx sending with an app PIN code in this step.

subs.add(askUserToConfirmTransactionSubject
	.subscribe((tx) => {
		// DISPLAY DIALOG with "Confirm TX" button, when clicked
		function onConfirmButtonClick() { // when button
			userDidConfirmTransactionSubject.next(tx)
		}
	}))

Submit TX

⚠️ Not yet implemented, subject to change.

When transaction is confirmed, either automatically or mannually by user, it is ready to be signed an submitted.

subs.add(userDidConfirmTransactionSubject.pipe(
	mergeMap((unsignedUserConfirmedTx) => radix.sign(unsignedUserConfirmedTx)),
	tap((signedTransaction) => {
		const txId = signedTransaction.id
		console.log(`🆔 transaction id: ${txId.toString()}`)
		pendingTransactionsSubject.next(signedTransaction)
	}),
	mergeMap((signedTransaction) => radix.submitSignedTransaction(signedTransaction)),
)
	.subscribe({ // Don't forget to subscribe
		error: (e) => {
			// Handle tx submission errors
			if (isSubmitTransactionError(e)) {
				switch (e) {
					case SubmitTransactionError.INVALID_SIGNATURE:
						console.log(`Failed to sign transaction, wrong account?`)
				}
			} else {
				console.log(`Unknown error submitting tx`)
			}
		},
	}))

Poll TX status

Now that the transaction has been submitted, we can proceed with polling the status of it.

const pollTxStatusTrigger = timer(5 * 1_000) // every 5 seconds

const transactionStatus$ = pollTxStatusTrigger
	.withLatestFrom(pendingTransactionsSubject)
	.pipe(
		mergeMap((signedTransaction) => 
			radix
				.ledger
				.statusOfTransactionById(signedTransaction.id)
				.pipe(
					map((status) => ({
						status,
						signedTransaction.id,
					})
				) 
			),
		)
	)

const transactionConfirmed$ = transactionStatus$.pipe(
	takeWhile(({ status, _ }) => status !== TransactionStatus.CONFIMRED),
)

subs.add(transactionStatus$
	.subscribe(({ status, id }) => console.log(`🔮 Status ${status.toString()} of tx with id: ${id.toString()}`)))

// 🔮 Status: INITIATED, 
// 🔮 Status: PENDING, 
// 🔮 Status: CONFIRMED, 

subs.add(transactionConfirmed$
	.subscribe(({ _, id }) => console.log(`✅ Tx with id ${id.toString()} confirmed`)))

// ✅ Status confirmed`

Ledger

All calls via .ledger returns failable Observables and any error will not be forwarded to the errors sink. Handle errors when subscribing to the Observable returned from method call.

☑️ Mocked implementation only 🤡.

This outlines all the requests you can make to the Radix Core API. All these requests are completely independent of any wallet, thus they have no notion of any "active address".

We have finished mocking all methods below.

tokenBalancesForAddress

☑️ Mocked implementation only 🤡.

Balance per token for specified address.

Method signature:

tokenBalancesForAddress: (address: AddressT) => Observable<TokenBalances>

transactionHistory

☑️ Mocked implementation only 🤡.

A page of the transaction history for the specified address. Pagination behaviour is controlled using input size and cursor.

Method signature:

transactionHistory: (
	input: Readonly<{
		address: AddressT
		size: number // must be larger than 0
		cursor?: string
	}>,
) => Observable<TransactionHistory>

nativeToken

☑️ Mocked implementation only 🤡.

Information about the native token of the Radix network.

Method signature:

nativeToken: () => Observable<Token>

tokenInfo

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

tokenInfo: (resourceIdentifier: ResourceIdentifierT) => Observable<Token>

stakesForAddress

☑️ Mocked implementation only 🤡.

Current stakes to validators for given address.

Method signature:

stakesForAddress: (address: AddressT) => Observable<StakePositions>

unstakesForAddress

☑️ Mocked implementation only 🤡.

Current unstakes from validators for given address.

Method signature:

unstakesForAddress: (address: AddressT) => Observable<UnstakePositions>

transactionStatus

☑️ Mocked implementation only 🤡.

transactionStatus: (id: TransactionIdentifierT) => Observable<StatusOfTransaction>

networkTransactionThroughput

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

networkTransactionThroughput: () => Observable<NetworkTransactionThroughput>

networkTransactionDemand

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

networkTransactionDemand: () => Observable<NetworkTransactionDemand>

buildTransaction

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

buildTransaction: (
	intent: TransactionIntent,
) => Observable<UnsignedTransaction>

submitSignedTransaction

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

submitSignedTransaction: (
	signedTransaction: SignedTransaction,
) => Observable<PendingTransaction>

validators

☑️ Mocked implementation only 🤡.

Information about specified token.

Method signature:

validators: (input: {
	// pagination
	size: number // must be larger than 0
	cursor?: AddressT // address of validator in last page
}): Observable<Validators>

lookupTransaction

☑️ Mocked implementation only 🤡.

Looks up an executed transaction by a txID. Observable will emit an error if no transaction matching the id is found.

Method signature:

lookupTransaction: (txID: TransactionIdentifierT): Observable<ExecutedTransaction>

networkId

☑️ Mocked implementation only 🤡.

Unique identifier for the network, part of each address (prefix).

Method signature:

networkId: () => Observable<Magic>

Unsubscribe

💡 Friendly reminder: when deemed appropriate, dispose of tour subscriptions by unsubscribe

It's impossible to say when appropriate, that is up to you.

// Earlier
const subs = new Subscription()

// Sometime you did
subs.add(someObservable.subscribe())

// Later
subs.unsubscribe()

Footnotes

1: At derivation path (BIP44) "m/44'/536'/0'/0/0'".

4.0.22

1 year ago

4.0.21

1 year ago

4.0.19

2 years ago

4.0.20

2 years ago

4.0.18

2 years ago

4.0.17

2 years ago

4.0.10

2 years ago

4.0.16

2 years ago

4.0.15

2 years ago

4.0.12

2 years ago

4.0.11

2 years ago

4.0.14

2 years ago

4.0.7

2 years ago

4.0.6

2 years ago

4.0.1

2 years ago

4.0.9

2 years ago

4.0.8

2 years ago

3.0.7

2 years ago

3.0.6

3 years ago

3.0.5

3 years ago

3.0.4

3 years ago

2.1.16

3 years ago

2.1.17

3 years ago

3.0.1

3 years ago

3.0.0

3 years ago

2.1.15

3 years ago

2.1.14

3 years ago

2.1.12

3 years ago

2.1.13

3 years ago

2.1.11

3 years ago

2.1.4

3 years ago

2.1.3

3 years ago

2.1.6

3 years ago

2.1.5

3 years ago

2.1.8

3 years ago

2.1.7

3 years ago

2.0.2

3 years ago

2.0.5

3 years ago

2.0.1

3 years ago

2.0.0

3 years ago

2.1.2

3 years ago

1.1.34

3 years ago

1.1.33

3 years ago

1.1.32

3 years ago

1.1.31

3 years ago

1.1.35

3 years ago

1.2.11

3 years ago

1.0.80

3 years ago

1.0.78

3 years ago

1.0.76

3 years ago

1.0.69

3 years ago

1.0.68

3 years ago

1.0.73

3 years ago

1.0.72

3 years ago

1.0.71

3 years ago

1.0.70

3 years ago

1.0.74

3 years ago

1.0.66

3 years ago

1.0.65

3 years ago

1.0.64

3 years ago

1.0.63

3 years ago

1.0.62

3 years ago

1.0.60

3 years ago

1.0.58

3 years ago

1.0.57

3 years ago

1.0.22

3 years ago

1.0.21

3 years ago

1.0.26

3 years ago

1.0.25

3 years ago

1.0.24

3 years ago

1.0.23

3 years ago

1.0.29

3 years ago

1.0.28

3 years ago

1.0.27

3 years ago

1.0.33

3 years ago

1.0.32

3 years ago

1.0.31

3 years ago

1.0.30

3 years ago

1.0.37

3 years ago

1.0.36

3 years ago

1.0.35

3 years ago

1.0.34

3 years ago

1.0.39

3 years ago

1.0.38

3 years ago

1.0.40

3 years ago

1.0.44

3 years ago

1.0.43

3 years ago

1.0.42

3 years ago

1.0.41

3 years ago

1.0.48

3 years ago

1.0.47

3 years ago

1.0.46

3 years ago

1.0.45

3 years ago

1.0.49

3 years ago

1.0.51

3 years ago

1.0.50

3 years ago

1.0.55

3 years ago

1.0.54

3 years ago

1.0.52

3 years ago

1.0.56

3 years ago

1.0.19

3 years ago

1.0.20

3 years ago

1.0.18

3 years ago

1.0.17

3 years ago

1.0.16

3 years ago

1.0.15

3 years ago

1.0.14

3 years ago

1.0.13

3 years ago

1.0.12

3 years ago

1.0.11

3 years ago

1.0.10

3 years ago

1.0.9

3 years ago

1.0.8

3 years ago

1.0.7

3 years ago

1.0.7-alpha.8

3 years ago

1.0.7-alpha.3

3 years ago

1.0.7-alpha.4

3 years ago

1.0.7-alpha.5

3 years ago

1.0.7-alpha.6

3 years ago

1.0.7-alpha.7

3 years ago

1.0.7-alpha.1

3 years ago

1.0.7-alpha.2

3 years ago

1.0.7-alpha.0

3 years ago

1.0.6-alpha.0

3 years ago

1.0.4-alpha.0

3 years ago

1.0.3-alpha.0

3 years ago

1.0.1-alpha.0

3 years ago

1.0.0-alpha.0

3 years ago