Skip to content
Documentation/canton-network-docs/SDKsWallet SDKUsing the SDKv0 to v1 migrationView on canton-network-docs
canton-network-docs/SDKsWallet SDKUsing the SDKv0 to v1 migration

Amulet Namespace

The amulet namespace is used for Canton coin specific operations.

Availability and extensibility

The amulet namespace is an extended namespace that requires configuration. You can initialize it either during SDK creation or later using the extend() 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()
Option 2: Add amulet namespace later using extend()
// 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

The AmuletConfig 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 services
  • validatorUrl: URL of the validator service
  • scanApiUrl: URL of the scan API
  • registryUrl: URL of the amulet registry

Key changes from v0 to v1

v0 used the tokenStandard 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 partyId explicitly to each operation
  • Initialize the namespace with configuration, which determines the instrumentAdmin and instrumentId
  • Access operations through logical groupings (traffic and preapproval)
Creating preapprovals
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,
      })
The below example demonstrates the full process of renewing and cancelling preapprovals:
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`)
}
See all 247 lines
Buy Member Traffic
const buyMemberTrafficCommand =
   await sdk.tokenStandard.buyMemberTraffic(
      senderPartyId,
      amount,
      participantId,
      inputUtxosOptional
   )

const [buyTrafficCommand, buyTrafficDisclosedContracts] =
   await sdk.amulet.traffic.buy({
      buyer,
      ccAmount,
      inputUtxos: [],
   })
Check Traffic Status
await sdk.tokenStandard.getMemberTrafficStatus(participantId)

await sdk.amulet.traffic.status()
Refer to the following example for more information:
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} `
//     )
// }
See all 148 lines
Tap The is useful for testing against LocalNet or Devnet.
await sdk.tokenStandard.createTap(partyId,
       amount,
       {
       instrumentId,
       instrumentAdmin
       })

await sdk.amulet.tap(partyId, amount)

Migration reference

v0 methodv1 method
sdk.tokenStandard.getMemberTrafficStatussdk.amulet.traffic.status
sdk.tokenStandard.buyMemberTrafficsdk.amulet.traffic.buy
sdk.userLedger.createTransferPreapprovalCommandsdk.amulet.preapproval.command.create
sdk.tokenStandard.getTransferPreApprovalByPartysdk.amulet.preapproval.fetchStatus
sdk.tokenStandard.createRenewTransferPreapprovalsdk.amulet.preapproval.renew
sdk.tokenStandard.createCancelTransferPreapprovalsdk.amulet.preapproval.command.cancel
sdk.tokenStandard.createTapsdk.amulet.tap
sdk.tokenStandard.lookupFeaturedAppssdk.amulet.featuredApp.rights
sdk.tokenStandard.selfGrantFeatureAppRightssdk.amulet.featuredApp.grant

See also