Availability and extensibility
The amulet namespace is an extended namespace that requires configuration. You can initialize it either during SDK creation or later using theextend() method.
Option 1: Initialize during SDK creation
const sdk = await SDK.create({
auth: authConfig,
ledgerClientUrl: 'http://localhost:2975',
amulet: {
validatorUrl: 'http://localhost:2000/api/validator',
scanApiUrl: 'http://localhost:2000/api/scan',
auth: amuletAuthConfig,
registryUrl: 'http://localhost:2000/api/registry'
}
})
// amulet namespace is now available
await sdk.amulet.traffic.status()
// Create basic SDK first
const basicSDK = await SDK.create({
auth: authConfig,
ledgerClientUrl: 'http://localhost:2975'
})
// Extend with amulet namespace when needed
const extendedSDK = await basicSDK.extend({
amulet: {
validatorUrl: 'http://localhost:2000/api/validator',
scanApiUrl: 'http://localhost:2000/api/scan',
auth: amuletAuthConfig,
registryUrl: 'http://localhost:2000/api/registry'
}
})
// Now amulet namespace is available
await extendedSDK.amulet.traffic.status()
Configuration
TheAmuletConfig type defines the configuration for the amulet namespace:
type AmuletConfig = {
auth: TokenProviderConfig
validatorUrl: string | URL
scanApiUrl: string | URL
registryUrl: URL
}
auth: Authentication configuration for accessing the validator and scan servicesvalidatorUrl: URL of the validator servicescanApiUrl: URL of the scan APIregistryUrl: URL of the amulet registry
Key changes from v0 to v1
v0 used thetokenStandard controller with implicit party context set via sdk.setPartyId() and the instrumentId and instrumentAdmin were passed in explicitly to each function.
v1 uses the amulet namespace where you:
- Pass
partyIdexplicitly to each operation - Initialize the namespace with configuration, which determines the instrumentAdmin and instrumentId
- Access operations through logical groupings (
trafficandpreapproval)
const transferPreApprovalProposal =
await sdk.userLedger?.createTransferPreapprovalCommand(
validatorOperatorParty!,
receiver?.partyId!,
instrumentAdminPartyId
)
await sdk.userLedger?.prepareSignExecuteAndWaitFor(
[transferPreApprovalProposal],
keyPairReceiver.privateKey,
v4()
)
const createPreapprovalCommand = await sdk.amulet.preapproval.command.create({
parties: {
receiver: partyId,
},
})
await sdk.ledger
.prepare({
partyId: partyId,
commands: createPreapprovalCommand,
})
.sign(privateKey)
.execute({
partyId: partyId,
})
Full example: create, renew, and cancel preapprovals
import { Holding, PrettyContract } from '@canton-network/core-tx-parser'
import { localNetStaticConfig, SDK } from '@canton-network/wallet-sdk'
import { pino } from 'pino'
import {
TOKEN_NAMESPACE_CONFIG,
TOKEN_PROVIDER_CONFIG_DEFAULT,
AMULET_NAMESPACE_CONFIG,
} from './utils/index.js'
const logger = pino({ name: 'v1-05-preapproval', level: 'info' })
const sdk = await SDK.create({
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL,
token: TOKEN_NAMESPACE_CONFIG,
amulet: AMULET_NAMESPACE_CONFIG,
})
await sdk.amulet.tapInternal('1000')
const aliceKeys = sdk.keys.generate()
const alice = await sdk.party.external
.create(aliceKeys.publicKey, {
partyHint: 'v1-05-alice',
})
.sign(aliceKeys.privateKey)
.execute()
const [amuletTapCommand, amuletTapDisclosedContracts] = await sdk.amulet.tap(
alice.partyId,
'10000'
)
await sdk.ledger
.prepare({
partyId: alice.partyId,
commands: amuletTapCommand,
disclosedContracts: amuletTapDisclosedContracts,
})
.sign(aliceKeys.privateKey)
.execute({ partyId: alice.partyId })
const bobKeys = sdk.keys.generate()
const bob = await sdk.party.external
.create(bobKeys.publicKey, {
partyHint: 'v1-05-bob',
})
.sign(bobKeys.privateKey)
.execute()
// --- TEST CREATE COMMAND
const createPreapprovalCommand = await sdk.amulet.preapproval.command.create({
parties: {
receiver: bob.partyId,
},
})
logger.info(
{ createPreapprovalCommand },
'Successfully created a preapproval command'
)
await sdk.ledger
.prepare({
partyId: bob.partyId,
commands: createPreapprovalCommand,
})
.sign(bobKeys.privateKey)
.execute({
partyId: bob.partyId,
})
logger.info('Successfully registered the preapproval.')
// --- TEST FETCH
const start = performance.now()
const fetchOnceStatus = await sdk.amulet.preapproval.fetchQuick(bob.partyId)
const end = performance.now()
const duration = end - start
if (duration < 1000) {
logger.info(
`Success! The operation was fast (${duration.toFixed(2)} ms) and fetchOnce status is ${fetchOnceStatus}.`
)
} else {
logger.warn(
`Warning: Operation took longer than 1 second (${(duration / 1000).toFixed(2)} s).`
)
}
logger.info('Fetching for preapproval status with retry')
const fetchedPreapprovalStatus = await sdk.amulet.preapproval.fetchStatus(
bob.partyId
)
logger.info({ fetchedPreapprovalStatus }, 'Fetched preapproval status')
const sentValue = 2000
const [transferCommand, transferDisclosedContracts] =
await sdk.token.transfer.create({
sender: alice.partyId,
recipient: bob.partyId,
amount: sentValue.toString(),
instrumentId: 'Amulet',
registryUrl: localNetStaticConfig.LOCALNET_REGISTRY_API_URL,
})
await sdk.ledger
.prepare({
partyId: alice.partyId,
commands: transferCommand,
disclosedContracts: transferDisclosedContracts,
})
.sign(aliceKeys.privateKey)
.execute({ partyId: alice.partyId })
logger.info({ sentValue }, 'Executed transfer from Alice to Bob with value:')
const aliceUtxos = await sdk.token.utxos.list({ partyId: alice.partyId })
const bobUtxos = await sdk.token.utxos.list({ partyId: bob.partyId })
const partyAmuletValue = (utxos: PrettyContract<Holding>[]) =>
utxos.reduce(
(acc, utxo) => acc + parseFloat(utxo.interfaceViewValue.amount),
0
)
const aliceAmuletValue = partyAmuletValue(aliceUtxos)
const bobAmuletValue = partyAmuletValue(bobUtxos)
if (aliceAmuletValue !== 8000 || bobAmuletValue !== 2000)
throw Error(
`Wrong end results for utxos: ${JSON.stringify({ aliceAmuletValue, bobAmuletValue })}`
)
logger.info({ aliceAmuletValue, bobAmuletValue }, 'Result:')
// --- TEST RENEW COMMAND
logger.info('Renewing preapproval...')
const start2 = performance.now()
const fetchOnceStatusWithPreapproval = await sdk.amulet.preapproval.fetchQuick(
bob.partyId
)
const end2 = performance.now()
const duration2 = end2 - start2
if (duration < 1000) {
logger.info(
`Success! The operation was fast (${duration2.toFixed(2)} ms) and fetchOnce status is ${fetchOnceStatusWithPreapproval}.`
)
} else {
logger.warn(
`Warning: Operation took longer than 1 second (${duration2.toFixed(2)} s).`
)
}
const newExpiresAt = new Date(fetchedPreapprovalStatus!.expiresAt)
newExpiresAt.setDate(newExpiresAt.getDate() + 2)
await sdk.amulet.preapproval.renew({
parties: {
receiver: bob.partyId,
},
expiresAt: newExpiresAt,
})
const fetchedStatusAfterRenew = await sdk.amulet.preapproval.fetchStatus(
bob.partyId,
{
oldCid: fetchedPreapprovalStatus!.contractId,
}
)
const before = fetchedPreapprovalStatus!.expiresAt
const after = fetchedStatusAfterRenew!.expiresAt
if (!(after.getTime() > before.getTime())) {
throw new Error(
`Expected expiresAt to increase after renewal. before=${fetchedPreapprovalStatus!.expiresAt.toISOString()} after=${fetchedStatusAfterRenew!.expiresAt.toISOString()}`
)
}
logger.info(
{
before: before.toISOString(),
after: after.toISOString(),
extendedSeconds: Math.round(
(after.getTime() - before.getTime()) / 1000
),
},
'TransferPreapproval expiry extended, managed to renew preapproval'
)
// --- TEST CANCEL COMMAND
logger.info('Testing out cancel command')
if (!fetchedStatusAfterRenew?.templateId) {
throw new Error('No preapproval found - fetchedPreapprovalStatus is null')
}
const [cancelPreapprovalCommand, cancelDisclosedContracts] =
await sdk.amulet.preapproval.command.cancel({
parties: {
receiver: bob.partyId,
},
})
if (!cancelPreapprovalCommand) {
throw Error(
'Cancel preapproval command is null even though one has been created before'
)
}
await sdk.ledger
.prepare({
partyId: bob.partyId,
commands: cancelPreapprovalCommand,
disclosedContracts: cancelDisclosedContracts,
})
.sign(bobKeys.privateKey)
.execute({
partyId: bob.partyId,
})
logger.info('Submitted cancel command; now polling')
const cancelled = await sdk.amulet.preapproval.fetchStatus(bob.partyId, {
cancelled: true,
})
const preapprovalACS = await sdk.ledger.acsReader.readJsContracts({
parties: [bob.partyId],
filterByParty: true,
})
const renewedPreapprovalStillActive = preapprovalACS.some(
(contract) => contract.contractId === fetchedStatusAfterRenew?.contractId
)
if (cancelled === null && !renewedPreapprovalStillActive) {
logger.info(`Successfully cancelled`)
}
const buyMemberTrafficCommand =
await sdk.tokenStandard.buyMemberTraffic(
senderPartyId,
amount,
participantId,
inputUtxosOptional
)
const [buyTrafficCommand, buyTrafficDisclosedContracts] =
await sdk.amulet.traffic.buy({
buyer,
ccAmount,
inputUtxos: [],
})
await sdk.tokenStandard.getMemberTrafficStatus(participantId)
await sdk.amulet.traffic.status()
Full example: buy member traffic
import pino from 'pino'
import { localNetStaticConfig, SDK } from '@canton-network/wallet-sdk'
import {
TOKEN_NAMESPACE_CONFIG,
TOKEN_PROVIDER_CONFIG_DEFAULT,
AMULET_NAMESPACE_CONFIG,
} from './utils/index.js'
const logger = pino({ name: 'v1-06-merge-utxos', level: 'info' })
const sdk = await SDK.create({
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL,
token: TOKEN_NAMESPACE_CONFIG,
amulet: AMULET_NAMESPACE_CONFIG,
})
const aliceKeys = sdk.keys.generate()
const alice = await sdk.party.external
.create(aliceKeys.publicKey, {
partyHint: 'v1-07-alice',
})
.sign(aliceKeys.privateKey)
.execute()
const bobKeys = sdk.keys.generate()
const bob = await sdk.party.external
.create(bobKeys.publicKey, {
partyHint: 'v1-07-bob',
})
.sign(bobKeys.privateKey)
.execute()
const createPreapprovalCommand = await sdk.amulet.preapproval.command.create({
parties: {
receiver: bob.partyId,
},
})
await sdk.ledger
.prepare({
partyId: bob.partyId,
commands: createPreapprovalCommand,
})
.sign(bobKeys.privateKey)
.execute({
partyId: bob.partyId,
})
// Mint holdings for alice
const [amuletTapCommand, amuletTapDisclosedContracts] = await sdk.amulet.tap(
alice.partyId,
'2000000'
)
await sdk.ledger
.prepare({
partyId: alice.partyId,
commands: amuletTapCommand,
disclosedContracts: amuletTapDisclosedContracts,
})
.sign(aliceKeys.privateKey)
.execute({ partyId: alice.partyId })
logger.info(`Tapped holdings for alice`)
const trafficStatusBeforePurchase = await sdk.amulet.traffic.status()
logger.info(
`Traffic status before purchase: ${JSON.stringify(trafficStatusBeforePurchase)}`
)
const ccAmount = 200000
const [buyTrafficCommand, buyTrafficDisclosedContracts] =
await sdk.amulet.traffic.buy({
buyer: alice.partyId,
ccAmount,
inputUtxos: [],
})
await sdk.ledger
.prepare({
partyId: alice.partyId,
commands: buyTrafficCommand,
disclosedContracts: buyTrafficDisclosedContracts,
})
.sign(aliceKeys.privateKey)
.execute({ partyId: alice.partyId })
logger.info(`buy member traffic for sender (${alice.partyId}) party completed`)
const utxos = await sdk.token.utxos.list({ partyId: alice.partyId })
logger.info(utxos, 'alice utxos')
const sentValue = 2000
const [transferCommand, transferDisclosedContracts] =
await sdk.token.transfer.create({
sender: alice.partyId,
recipient: bob.partyId,
amount: sentValue.toString(),
instrumentId: 'Amulet',
registryUrl: localNetStaticConfig.LOCALNET_REGISTRY_API_URL,
})
await sdk.ledger
.prepare({
partyId: alice.partyId,
commands: transferCommand,
disclosedContracts: transferDisclosedContracts,
})
.sign(aliceKeys.privateKey)
.execute({ partyId: alice.partyId })
//TODO: This does not work when we run multiple code snippets parallel
// await new Promise((resolve) => setTimeout(resolve, 61_000))
// const trafficStatusAfterPurchaseAndSomeTime = await amulet.traffic.status()
// const difference =
// trafficStatusAfterPurchaseAndSomeTime.traffic_status.target
// .total_purchased -
// trafficStatusBeforePurchase.traffic_status.target.total_purchased
// if (difference === ccAmount) {
// logger.info(
// {
// trafficStatusBeforePurchase,
// trafficStatusAfterPurchaseAndSomeTime,
// },
// 'MemberTraffic status. Traffic purchased successfully'
// )
// } else {
// logger.error(
// {
// trafficStatusBeforePurchase,
// trafficStatusAfterPurchaseAndSomeTime,
// },
// 'MemberTraffic status.'
// )
// throw new Error(
// `Member traffic difference is ${difference}, expected ${ccAmount} `
// )
// }
await sdk.tokenStandard.createTap(partyId,
amount,
{
instrumentId,
instrumentAdmin
})
await sdk.amulet.tap(partyId, amount)
Migration reference
| v0 method | v1 method |
|---|---|
sdk.tokenStandard.getMemberTrafficStatus | sdk.amulet.traffic.status |
sdk.tokenStandard.buyMemberTraffic | sdk.amulet.traffic.buy |
sdk.userLedger.createTransferPreapprovalCommand | sdk.amulet.preapproval.command.create |
sdk.tokenStandard.getTransferPreApprovalByParty | sdk.amulet.preapproval.fetchStatus |
sdk.tokenStandard.createRenewTransferPreapproval | sdk.amulet.preapproval.renew |
sdk.tokenStandard.createCancelTransferPreapproval | sdk.amulet.preapproval.command.cancel |
sdk.tokenStandard.createTap | sdk.amulet.tap |
sdk.tokenStandard.lookupFeaturedApps | sdk.amulet.featuredApp.rights |
sdk.tokenStandard.selfGrantFeatureAppRights | sdk.amulet.featuredApp.grant |
See also
- v0 to v1 migration overview
- Migration cheat sheet
- Configuration — SDK configuration
- Performing a CC tap — Canton Coin tap guide
- Token namespace — Token transfers and UTXOs