import { C } from "../core/mod.ts"; import { coreToUtxo, createCostModels, fromHex, fromUnit, paymentCredentialOf, toHex, toUnit, Utils, utxoToCore, } from "../utils/mod.ts"; import { Address, Credential, Delegation, ExternalWallet, Json, KeyHash, Network, OutRef, Payload, PrivateKey, Provider, RewardAddress, SignedMessage, Slot, Transaction, TxHash, Unit, UTxO, Wallet, WalletApi, } from "../types/mod.ts"; import { Tx } from "./tx.ts"; import { TxComplete } from "./tx_complete.ts"; import { discoverOwnUsedTxKeyHashes, walletFromSeed } from "../misc/wallet.ts"; import { signData, verifyData } from "../misc/sign_data.ts"; import { Message } from "./message.ts"; import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts"; import { Constr, Data } from "../plutus/data.ts"; import { Emulator } from "../provider/emulator.ts"; export class Translucent { txBuilderConfig!: C.TransactionBuilderConfig; wallet!: Wallet; provider!: Provider; network: Network = "Mainnet"; utils!: Utils; static async new( provider?: Provider, network?: Network, ): Promise { const translucent = new this(); if (network) translucent.network = network; if (provider) { translucent.provider = provider; const protocolParameters = await provider.getProtocolParameters(); if (translucent.provider instanceof Emulator) { translucent.network = "Custom"; SLOT_CONFIG_NETWORK[translucent.network] = { zeroTime: translucent.provider.now(), zeroSlot: 0, slotLength: 1000, }; } const slotConfig = SLOT_CONFIG_NETWORK[translucent.network]; translucent.txBuilderConfig = C.TransactionBuilderConfigBuilder.new() .coins_per_utxo_byte( C.BigNum.from_str(protocolParameters.coinsPerUtxoByte.toString()), ) .fee_algo( C.LinearFee.new( C.BigNum.from_str(protocolParameters.minFeeA.toString()), C.BigNum.from_str(protocolParameters.minFeeB.toString()), ), ) .key_deposit( C.BigNum.from_str(protocolParameters.keyDeposit.toString()), ) .pool_deposit( C.BigNum.from_str(protocolParameters.poolDeposit.toString()), ) .max_tx_size(protocolParameters.maxTxSize) .max_value_size(protocolParameters.maxValSize) .collateral_percentage(protocolParameters.collateralPercentage) .max_collateral_inputs(protocolParameters.maxCollateralInputs) .ex_unit_prices( C.ExUnitPrices.new( C.UnitInterval.new( C.BigNum.from_str(protocolParameters.priceMem[0].toString()), C.BigNum.from_str(protocolParameters.priceMem[1].toString()), ), C.UnitInterval.new( C.BigNum.from_str(protocolParameters.priceStep[0].toString()), C.BigNum.from_str(protocolParameters.priceStep[1].toString()), ), ), ) .costmdls(createCostModels(protocolParameters.costModels)) .build(); } translucent.utils = new Utils(translucent); return translucent; } /** * Switch provider and/or network. * If provider or network unset, no overwriting happens. Provider or network from current instance are taken then. */ async switchProvider( provider?: Provider, network?: Network, ): Promise { if (this.network === "Custom") { throw new Error("Cannot switch when on custom network."); } const translucent = await Translucent.new(provider, network); this.txBuilderConfig = translucent.txBuilderConfig; this.provider = provider || this.provider; this.network = network || this.network; this.wallet = translucent.wallet; return this; } newTx(): Tx { return new Tx(this); } fromTx(tx: Transaction): TxComplete { return new TxComplete(this, C.Transaction.from_bytes(fromHex(tx))); } /** Signs a message. Expects the payload to be Hex encoded. */ newMessage(address: Address | RewardAddress, payload: Payload): Message { return new Message(this, address, payload); } /** Verify a message. Expects the payload to be Hex encoded. */ verifyMessage( address: Address | RewardAddress, payload: Payload, signedMessage: SignedMessage, ): boolean { const { paymentCredential, stakeCredential, address: { hex: addressHex }, } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash || stakeCredential?.hash; if (!keyHash) throw new Error("Not a valid address provided."); return verifyData(addressHex, keyHash, payload, signedMessage); } currentSlot(): Slot { return this.utils.unixTimeToSlot(Date.now()); } utxosAt(addressOrCredential: Address | Credential): Promise { return this.provider.getUtxos(addressOrCredential); } utxosAtWithUnit( addressOrCredential: Address | Credential, unit: Unit, ): Promise { return this.provider.getUtxosWithUnit(addressOrCredential, unit); } /** Unit needs to be an NFT (or optionally the entire supply in one UTxO). */ utxoByUnit(unit: Unit): Promise { return this.provider.getUtxoByUnit(unit); } utxosByOutRef(outRefs: Array): Promise { return this.provider.getUtxosByOutRef(outRefs); } delegationAt(rewardAddress: RewardAddress): Promise { return this.provider.getDelegation(rewardAddress); } awaitTx(txHash: TxHash, checkInterval = 3000): Promise { return this.provider.awaitTx(txHash, checkInterval); } async datumOf(utxo: UTxO, type?: T): Promise { if (!utxo.datum) { if (!utxo.datumHash) { throw new Error("This UTxO does not have a datum hash."); } utxo.datum = await this.provider.getDatum(utxo.datumHash); } return Data.from(utxo.datum, type); } /** Query CIP-0068 metadata for a specifc asset. */ async metadataOf(unit: Unit): Promise { const { policyId, name, label } = fromUnit(unit); switch (label) { case 222: case 333: case 444: { const utxo = await this.utxoByUnit(toUnit(policyId, name, 100)); const metadata = (await this.datumOf(utxo)) as Constr; return Data.toJson(metadata.fields[0]); } default: throw new Error("No variant matched."); } } /** * Cardano Private key in bech32; not the BIP32 private key or any key that is not fully derived. * Only an Enteprise address (without stake credential) is derived. */ selectWalletFromPrivateKey(privateKey: PrivateKey): Translucent { const priv = C.PrivateKey.from_bech32(privateKey); const pubKeyHash = priv.to_public().hash(); this.wallet = { // deno-lint-ignore require-await address: async (): Promise
=> C.EnterpriseAddress.new( this.network === "Mainnet" ? 1 : 0, C.StakeCredential.from_keyhash(pubKeyHash), ) .to_address() .to_bech32(undefined), // deno-lint-ignore require-await rewardAddress: async (): Promise => null, getUtxos: async (): Promise => { return await this.utxosAt( paymentCredentialOf(await this.wallet.address()), ); }, getUtxosCore: async (): Promise => { const utxos = await this.utxosAt( paymentCredentialOf(await this.wallet.address()), ); const coreUtxos = C.TransactionUnspentOutputs.new(); utxos.forEach((utxo) => { coreUtxos.add(utxoToCore(utxo)); }); return coreUtxos; }, // deno-lint-ignore require-await getDelegation: async (): Promise => { return { poolId: null, rewards: 0n }; }, // deno-lint-ignore require-await signTx: async (tx: C.Transaction): Promise => { const witness = C.make_vkey_witness( C.hash_transaction(tx.body()), priv, ); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); txWitnessSetBuilder.add_vkey(witness); return txWitnessSetBuilder.build(); }, // deno-lint-ignore require-await signMessage: async ( address: Address | RewardAddress, payload: Payload, ): Promise => { const { paymentCredential, address: { hex: hexAddress }, } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash; const originalKeyHash = pubKeyHash.to_hex(); if (!keyHash || keyHash !== originalKeyHash) { throw new Error(`Cannot sign message for address: ${address}.`); } return signData(hexAddress, payload, privateKey); }, submitTx: async (tx: Transaction): Promise => { return await this.provider.submitTx(tx); }, }; return this; } selectWallet(api: WalletApi): Translucent { const getAddressHex = async () => { const [addressHex] = await api.getUsedAddresses(); if (addressHex) return addressHex; const [unusedAddressHex] = await api.getUnusedAddresses(); return unusedAddressHex; }; this.wallet = { address: async (): Promise
=> C.Address.from_bytes(fromHex(await getAddressHex())).to_bech32( undefined, ), rewardAddress: async (): Promise => { const [rewardAddressHex] = await api.getRewardAddresses(); const rewardAddress = rewardAddressHex ? C.RewardAddress.from_address( C.Address.from_bytes(fromHex(rewardAddressHex)), )! .to_address() .to_bech32(undefined) : null; return rewardAddress; }, getUtxos: async (): Promise => { const utxos = ((await api.getUtxos()) || []).map((utxo) => { const parsedUtxo = C.TransactionUnspentOutput.from_bytes( fromHex(utxo), ); return coreToUtxo(parsedUtxo); }); return utxos; }, getUtxosCore: async (): Promise => { const utxos = C.TransactionUnspentOutputs.new(); ((await api.getUtxos()) || []).forEach((utxo) => { utxos.add(C.TransactionUnspentOutput.from_bytes(fromHex(utxo))); }); return utxos; }, getDelegation: async (): Promise => { const rewardAddr = await this.wallet.rewardAddress(); return rewardAddr ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, signTx: async (tx: C.Transaction): Promise => { const witnessSet = await api.signTx(toHex(tx.to_bytes()), true); return C.TransactionWitnessSet.from_bytes(fromHex(witnessSet)); }, signMessage: async ( address: Address | RewardAddress, payload: Payload, ): Promise => { const hexAddress = toHex(C.Address.from_bech32(address).to_bytes()); return await api.signData(hexAddress, payload); }, submitTx: async (tx: Transaction): Promise => { const txHash = await api.submitTx(tx); return txHash; }, }; return this; } /** * Emulates a wallet by constructing it with the utxos and an address. * If utxos are not set, utxos are fetched from the provided address. */ selectWalletFrom({ address, utxos, rewardAddress, }: ExternalWallet): Translucent { const addressDetails = this.utils.getAddressDetails(address); this.wallet = { // deno-lint-ignore require-await address: async (): Promise
=> address, // deno-lint-ignore require-await rewardAddress: async (): Promise => { const rewardAddr = !rewardAddress && addressDetails.stakeCredential ? (() => { if (addressDetails.stakeCredential.type === "Key") { return C.RewardAddress.new( this.network === "Mainnet" ? 1 : 0, C.StakeCredential.from_keyhash( C.Ed25519KeyHash.from_hex( addressDetails.stakeCredential.hash, ), ), ) .to_address() .to_bech32(undefined); } return C.RewardAddress.new( this.network === "Mainnet" ? 1 : 0, C.StakeCredential.from_scripthash( C.ScriptHash.from_hex(addressDetails.stakeCredential.hash), ), ) .to_address() .to_bech32(undefined); })() : rewardAddress; return rewardAddr || null; }, getUtxos: async (): Promise => { return utxos ? utxos : await this.utxosAt(paymentCredentialOf(address)); }, getUtxosCore: async (): Promise => { const coreUtxos = C.TransactionUnspentOutputs.new(); (utxos ? utxos : await this.utxosAt(paymentCredentialOf(address)) ).forEach((utxo) => coreUtxos.add(utxoToCore(utxo))); return coreUtxos; }, getDelegation: async (): Promise => { const rewardAddr = await this.wallet.rewardAddress(); return rewardAddr ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, // deno-lint-ignore require-await signTx: async (): Promise => { throw new Error("Not implemented"); }, // deno-lint-ignore require-await signMessage: async (): Promise => { throw new Error("Not implemented"); }, submitTx: async (tx: Transaction): Promise => { return await this.provider.submitTx(tx); }, }; return this; } /** * Select wallet from a seed phrase (e.g. 15 or 24 words). You have the option to choose between a Base address (with stake credential) * and Enterprise address (without stake credential). You can also decide which account index to derive. By default account 0 is derived. */ selectWalletFromSeed( seed: string, options?: { addressType?: "Base" | "Enterprise"; accountIndex?: number; password?: string; }, ): Translucent { const { address, rewardAddress, paymentKey, stakeKey } = walletFromSeed( seed, { addressType: options?.addressType || "Base", accountIndex: options?.accountIndex || 0, password: options?.password, network: this.network, }, ); const paymentKeyHash = C.PrivateKey.from_bech32(paymentKey) .to_public() .hash() .to_hex(); const stakeKeyHash = stakeKey ? C.PrivateKey.from_bech32(stakeKey).to_public().hash().to_hex() : ""; const privKeyHashMap = { [paymentKeyHash]: paymentKey, [stakeKeyHash]: stakeKey, }; this.wallet = { // deno-lint-ignore require-await address: async (): Promise
=> address, // deno-lint-ignore require-await rewardAddress: async (): Promise => rewardAddress || null, // deno-lint-ignore require-await getUtxos: async (): Promise => this.utxosAt(paymentCredentialOf(address)), getUtxosCore: async (): Promise => { const coreUtxos = C.TransactionUnspentOutputs.new(); (await this.utxosAt(paymentCredentialOf(address))).forEach((utxo) => coreUtxos.add(utxoToCore(utxo)), ); return coreUtxos; }, getDelegation: async (): Promise => { const rewardAddr = await this.wallet.rewardAddress(); return rewardAddr ? await this.delegationAt(rewardAddr) : { poolId: null, rewards: 0n }; }, signTx: async (tx: C.Transaction): Promise => { const utxos = await this.utxosAt(address); const ownKeyHashes: Array = [paymentKeyHash, stakeKeyHash]; const usedKeyHashes = discoverOwnUsedTxKeyHashes( tx, ownKeyHashes, utxos, ); const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); usedKeyHashes.forEach((keyHash) => { const witness = C.make_vkey_witness( C.hash_transaction(tx.body()), C.PrivateKey.from_bech32(privKeyHashMap[keyHash]!), ); txWitnessSetBuilder.add_vkey(witness); }); return txWitnessSetBuilder.build(); }, // deno-lint-ignore require-await signMessage: async ( address: Address | RewardAddress, payload: Payload, ): Promise => { const { paymentCredential, stakeCredential, address: { hex: hexAddress }, } = this.utils.getAddressDetails(address); const keyHash = paymentCredential?.hash || stakeCredential?.hash; const privateKey = privKeyHashMap[keyHash!]; if (!privateKey) { throw new Error(`Cannot sign message for address: ${address}.`); } return signData(hexAddress, payload, privateKey); }, submitTx: async (tx: Transaction): Promise => { return await this.provider.submitTx(tx); }, }; return this; } }