use canton_auth::{OidcConfig, TokenProvider};
use canton_daml as rt;
use canton_ledger::{CantonClient, Config};
use canton_splice_api_token_holding_v2::splice_api_token_holding_v2::Splice_Api_Token_HoldingV2 as h;
use canton_splice_api_token_metadata_v1::splice_api_token_metadata_v1::Splice_Api_Token_MetadataV1 as md;
use canton_splice_api_token_transfer_instruction_v2::splice_api_token_transfer_instruction_v2::Splice_Api_Token_TransferInstructionV2 as ti;
use canton_token::{RegistryClient, TransferKind};
fn var(name: &str) -> Result<String, String> {
std::env::var(name).map_err(|_| format!("set {name}"))
}
fn authenticate(config: Config) -> Config {
match (
std::env::var("CANTON_TOKEN"),
std::env::var("CANTON_TEST_TOKEN_URL"),
std::env::var("CANTON_TEST_CLIENT_ID"),
std::env::var("CANTON_TEST_CLIENT_SECRET"),
) {
(Ok(token), ..) => config.with_token(token),
(_, Ok(url), Ok(id), Ok(secret)) => {
let mut oidc = OidcConfig::new(url, id, secret);
if let Ok(audience) = std::env::var("CANTON_TEST_AUDIENCE") {
oidc = oidc.with_audience(audience);
}
config.with_oidc(TokenProvider::new(oidc))
}
_ => config,
}
}
fn account(owner: rt::Party, id: String) -> h::Account {
h::Account {
owner: Some(owner),
provider: None,
id,
}
}
async fn pick_holdings<T>(
client: &CantonClient,
sender: &str,
instrument_id: &str,
) -> Result<Vec<rt::ContractId<T>>, Box<dyn std::error::Error>> {
let raw = std::env::var("CANTON_TOKEN_HOLDINGS").unwrap_or_default();
if !raw.trim().is_empty() {
return Ok(raw
.split(',')
.map(str::trim)
.filter(|id| !id.is_empty())
.map(|id| rt::ContractId::new(id.to_string()))
.collect());
}
let spendable = canton_token::holdings::spendable(client, sender, instrument_id).await?;
if spendable.is_empty() {
return Err(format!("{sender} has no unlocked holdings of {instrument_id}").into());
}
println!(
"holdings: {} unlocked holding(s) of {instrument_id} read from the ledger",
spendable.len()
);
Ok(spendable.iter().map(HoldingSummaryExt::typed).collect())
}
trait HoldingSummaryExt {
fn typed<T>(&self) -> rt::ContractId<T>;
}
impl HoldingSummaryExt for canton_token::holdings::HoldingSummary {
fn typed<T>(&self) -> rt::ContractId<T> {
canton_token::holdings::HoldingSummary::typed(self)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let registry = RegistryClient::new(&var("CANTON_TOKEN_REGISTRY_URL")?)?;
let info = registry.info().await?;
let admin = rt::Party::parse(&info.admin_id)?;
println!("registry admin: {}", info.admin_id);
let instrument_id = var("CANTON_TOKEN_INSTRUMENT")?;
match registry.instrument(&instrument_id).await? {
Some(instrument) => println!(
"instrument: {} ({}), {} decimals",
instrument.name, instrument.symbol, instrument.decimals
),
None => println!("instrument: {instrument_id} — this registry does not issue it"),
}
let sender = rt::Party::parse(&var("CANTON_TOKEN_SENDER")?)?;
let client =
CantonClient::connect_lazy(authenticate(Config::new(var("CANTON_TEST_ENDPOINT")?)))?;
let receiver = rt::Party::parse(&var("CANTON_TOKEN_RECEIVER")?)?;
let sender_account = account(
sender.clone(),
std::env::var("CANTON_TOKEN_SENDER_ACCOUNT").unwrap_or_default(),
);
let receiver_account = account(
receiver,
std::env::var("CANTON_TOKEN_RECEIVER_ACCOUNT").unwrap_or_default(),
);
println!(
"from account: {:?} / to account: {:?}",
sender_account.id, receiver_account.id
);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_micros();
let now = i64::try_from(now)?;
let inputs = pick_holdings(&client, sender.as_str(), &instrument_id).await?;
let transfer = ti::Transfer {
sender: sender_account,
receiver: receiver_account,
amount: var("CANTON_TOKEN_AMOUNT")?.parse()?,
instrument_id: h::InstrumentId {
admin,
id: instrument_id,
},
requested_at: rt::Timestamp(now),
execute_before: rt::Timestamp(now + 10 * 60 * 1_000_000),
input_holding_cids: inputs,
meta: md::Metadata {
values: rt::TextMap::new(),
},
};
let command = canton_token::v2::transfer(®istry, transfer, vec![sender.clone()]).await?;
match command.transfer_kind() {
Some(TransferKind::Direct) => println!("kind: direct — completes on submission"),
Some(TransferKind::SelfTransfer) => {
println!("kind: self — sender and receiver match");
}
Some(TransferKind::Offer) => {
println!("kind: offer — this will NOT settle until the receiver accepts it");
}
None => println!("kind: the registry did not say"),
Some(other) => println!("kind: {other:?} — a kind this build does not know"),
}
println!(
"disclosing: {} contract(s) the registry named",
command.disclosed_contracts().len()
);
if std::env::var_os("CANTON_TOKEN_DRY_RUN").is_some() {
println!("dry run: the command is built and not submitted");
return Ok(());
}
let transaction = client
.submit_and_wait_for_transaction(command.into_submit(sender.as_str()))
.await?;
println!(
"committed {} at offset {} with {} event(s)",
transaction.update_id,
transaction.offset,
transaction.events.len()
);
for change in canton_token::v2::events::holdings_changes(&transaction.events)? {
println!(
" holdings change on {} (node {}): {} spent, {} produced, {} leg(s)",
change.contract_id,
change.node_id,
change.change.input_holding_cids.len(),
change.change.output_holding_cids.len(),
change.change.transfer_leg_sides.len(),
);
}
Ok(())
}