use canton_auth::{OidcConfig, TokenProvider};
use canton_daml as rt;
use canton_ledger::{CantonClient, Config};
use canton_splice_api_token_holding_v1::splice_api_token_holding_v1::Splice_Api_Token_HoldingV1 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_v1::splice_api_token_transfer_instruction_v1::Splice_Api_Token_TransferInstructionV1 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,
}
}
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 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.clone(),
receiver: rt::Party::parse(&var("CANTON_TOKEN_RECEIVER")?)?,
amount: var("CANTON_TOKEN_AMOUNT")?.parse()?,
instrument_id: h::InstrumentId {
admin: admin.clone(),
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::transfer(®istry, &admin, transfer).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()
);
Ok(())
}