diff --git a/src/translucent/translucent.ts b/src/translucent/translucent.ts index de9c58d..6cbaf58 100644 --- a/src/translucent/translucent.ts +++ b/src/translucent/translucent.ts @@ -1,11 +1,9 @@ import { C } from "../core/mod.ts"; import { - coreToUtxo, createCostModels, fromHex, fromUnit, paymentCredentialOf, - toHex, toUnit, Utils, utxoToCore, @@ -14,7 +12,6 @@ import { Address, Credential, Delegation, - ExternalWallet, Json, KeyHash, Network, @@ -29,7 +26,6 @@ import { TxHash, Unit, UTxO, - Wallet, WalletApi, } from "../types/mod.ts"; import { Tx } from "./tx.ts"; @@ -40,10 +36,16 @@ 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"; +import { toCore } from "../utils/to.ts"; +import { WalletConnector } from "../wallets/wallet_connector.ts"; +import { AbstractWallet } from "../wallets/abstract.ts"; +import { PrivateKeyWallet } from "../wallets/private_key.ts"; +import { SeedWallet } from "../wallets/seed.ts"; +import { ExternalWallet } from "../wallets/public_wallet.ts"; export class Translucent { txBuilderConfig!: C.TransactionBuilderConfig; - wallet!: Wallet; + wallet!: AbstractWallet; provider!: Provider; network: Network = "Mainnet"; utils!: Utils; @@ -214,226 +216,24 @@ export class Translucent { } } - /** - * 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; + return this.useWallet(new PrivateKeyWallet(this, privateKey)); } 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; + return this.useWallet(new WalletConnector(this, api)); } - /** - * 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; + selectWalletFrom( + address: Address, + utxos?: UTxO[], + rewardAddress?: RewardAddress, + ): Translucent { + return this.useWallet( + new ExternalWallet(this, address, utxos, rewardAddress), + ); } - /** - * 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?: { @@ -442,98 +242,11 @@ export class Translucent { 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 this.useWallet(new SeedWallet(this, seed, options)); + } - return signData(hexAddress, payload, privateKey); - }, - submitTx: async (tx: Transaction): Promise => { - return await this.provider.submitTx(tx); - }, - }; + useWallet(wallet: AbstractWallet) { + this.wallet = wallet; return this; } } diff --git a/src/translucent/tx.ts b/src/translucent/tx.ts index bb594f1..948fe87 100644 --- a/src/translucent/tx.ts +++ b/src/translucent/tx.ts @@ -36,6 +36,7 @@ import { applyDoubleCborEncoding } from "../utils/utils.ts"; import { Translucent } from "./translucent.ts"; import { TxComplete } from "./tx_complete.ts"; import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts"; +import { toCore } from "../utils/to.ts"; type ScriptOrRef = | { inlineScript: C.PlutusScript } @@ -365,18 +366,7 @@ export class Tx { if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } - const credential = - addressDetails.stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ); + const credential = toCore.credential(addressDetails.stakeCredential); let certBuilder = C.SingleCertificateBuilder.new( C.Certificate.new_stake_delegation( @@ -439,18 +429,7 @@ export class Tx { if (addressDetails.type !== "Reward" || !addressDetails.stakeCredential) { throw new Error("Not a reward address provided."); } - const credential = - addressDetails.stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_bytes( - fromHex(addressDetails.stakeCredential.hash), - ), - ); + const credential = toCore.credential(addressDetails.stakeCredential); that.txBuilder.add_cert( C.SingleCertificateBuilder.new( diff --git a/src/types/types.ts b/src/types/types.ts index fb924b7..8547fdf 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -159,32 +159,8 @@ export type Delegation = { rewards: Lovelace; }; -/** - * A wallet that can be constructed from external data e.g utxos and an address. - * It doesn't allow you to sign transactions/messages. This needs to be handled separately. - */ -export interface ExternalWallet { - address: Address; - utxos?: UTxO[]; - rewardAddress?: RewardAddress; -} - export type SignedMessage = { signature: string; key: string }; -export interface Wallet { - address(): Promise
; - rewardAddress(): Promise; - getUtxos(): Promise; - getUtxosCore(): Promise; - getDelegation(): Promise; - signTx(tx: C.Transaction): Promise; - signMessage( - address: Address | RewardAddress, - payload: Payload, - ): Promise; - submitTx(signedTx: Transaction): Promise; -} - /** JSON object */ // deno-lint-ignore no-explicit-any export type Json = any; diff --git a/src/utils/to.ts b/src/utils/to.ts new file mode 100644 index 0000000..ca0ac7c --- /dev/null +++ b/src/utils/to.ts @@ -0,0 +1,17 @@ +import * as C from "@dcspark/cardano-multiplatform-lib-nodejs"; +import { Credential } from "../mod"; + +export const toCore = { + credential(credential: Credential): C.StakeCredential { + if (credential.type == "Key") { + return C.StakeCredential.from_keyhash( + C.Ed25519KeyHash.from_hex(credential.hash), + ); + } else if (credential.type == "Script") { + return C.StakeCredential.from_scripthash( + C.ScriptHash.from_hex(credential.hash), + ); + } + throw new Error("Lucid credential type mismatch"); + }, +}; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 27d3e3e..ec9eff3 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -35,6 +35,8 @@ import { unixTimeToEnclosingSlot, } from "../plutus/time.ts"; import { Data } from "../plutus/data.ts"; +import { toCore } from "./to"; + export class Utils { private translucent: Translucent; constructor(translucent: Translucent) { @@ -50,13 +52,7 @@ export class Utils { return C.BaseAddress.new( networkToId(this.translucent.network), C.StakeCredential.from_scripthash(C.ScriptHash.from_hex(validatorHash)), - stakeCredential.type === "Key" - ? C.StakeCredential.from_keyhash( - C.Ed25519KeyHash.from_hex(stakeCredential.hash), - ) - : C.StakeCredential.from_scripthash( - C.ScriptHash.from_hex(stakeCredential.hash), - ), + toCore.credential(stakeCredential), ) .to_address() .to_bech32(undefined); diff --git a/src/wallets/abstract.ts b/src/wallets/abstract.ts new file mode 100644 index 0000000..8de38e3 --- /dev/null +++ b/src/wallets/abstract.ts @@ -0,0 +1,25 @@ +import { + UTxO, + C, + Delegation, + Payload, + TxHash, + Address, + RewardAddress, + Transaction, + SignedMessage, +} from "../mod"; + +export abstract class AbstractWallet { + abstract address(): Promise
; + abstract rewardAddress(): Promise; + abstract getUtxos(): Promise; + abstract getUtxosCore(): Promise; + abstract getDelegation(): Promise; + abstract signTx(tx: C.Transaction): Promise; + abstract signMessage( + address: Address | RewardAddress, + payload: Payload, + ): Promise; + abstract submitTx(signedTx: Transaction): Promise; +} diff --git a/src/wallets/private_key.ts b/src/wallets/private_key.ts new file mode 100644 index 0000000..2b42428 --- /dev/null +++ b/src/wallets/private_key.ts @@ -0,0 +1,97 @@ +import { signData } from "../misc/sign_data"; +import { + SignedMessage, + PrivateKey, + Address, + RewardAddress, + Transaction, + Translucent, + WalletApi, + C, + UTxO, + paymentCredentialOf, + utxoToCore, + Delegation, + Payload, + TxHash, +} from "../mod"; +import { AbstractWallet } from "./abstract"; + +/** + * 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. + */ +export class PrivateKeyWallet implements AbstractWallet { + translucent: Translucent; + private privateKey: PrivateKey; + private priv: C.PrivateKey; + pubKeyHash: C.Ed25519KeyHash; + + constructor(translucent: Translucent, privateKey: PrivateKey) { + this.translucent = translucent; + this.privateKey = privateKey; + this.priv = C.PrivateKey.from_bech32(privateKey); + this.pubKeyHash = this.priv.to_public().hash(); + } + + async address(): Promise
{ + return C.EnterpriseAddress.new( + this.translucent.network === "Mainnet" ? 1 : 0, + C.StakeCredential.from_keyhash(this.pubKeyHash), + ) + .to_address() + .to_bech32(undefined); + } + // deno-lint-ignore require-await + async rewardAddress(): Promise { + return null; + } + async getUtxos(): Promise { + return await this.translucent.utxosAt( + paymentCredentialOf(await this.address()), + ); + } + async getUtxosCore(): Promise { + const utxos = await this.translucent.utxosAt( + paymentCredentialOf(await this.address()), + ); + const coreUtxos = C.TransactionUnspentOutputs.new(); + utxos.forEach((utxo) => { + coreUtxos.add(utxoToCore(utxo)); + }); + return coreUtxos; + } + // deno-lint-ignore require-await + async getDelegation(): Promise { + return { poolId: null, rewards: 0n }; + } + // deno-lint-ignore require-await + async signTx(tx: C.Transaction): Promise { + const witness = C.make_vkey_witness( + C.hash_transaction(tx.body()), + this.priv, + ); + const txWitnessSetBuilder = C.TransactionWitnessSetBuilder.new(); + txWitnessSetBuilder.add_vkey(witness); + return txWitnessSetBuilder.build(); + } + // deno-lint-ignore require-await + async signMessage( + address: Address | RewardAddress, + payload: Payload, + ): Promise { + const { + paymentCredential, + address: { hex: hexAddress }, + } = this.translucent.utils.getAddressDetails(address); + const keyHash = paymentCredential?.hash; + const originalKeyHash = this.pubKeyHash.to_hex(); + if (!keyHash || keyHash !== originalKeyHash) { + throw new Error(`Cannot sign message for address: ${address}.`); + } + return signData(hexAddress, payload, this.privateKey); + } + async submitTx(tx: Transaction): Promise { + return await this.translucent.provider.submitTx(tx); + } +} diff --git a/src/wallets/public_wallet.ts b/src/wallets/public_wallet.ts new file mode 100644 index 0000000..9a79ab3 --- /dev/null +++ b/src/wallets/public_wallet.ts @@ -0,0 +1,96 @@ +import { + C, + UTxO, + paymentCredentialOf, + utxoToCore, + Delegation, + TxHash, + Address, + RewardAddress, + Transaction, + SignedMessage, + Translucent, + AddressDetails, +} from "../mod"; +import { toCore } from "../utils/to"; +import { AbstractWallet } from "./abstract"; + +/** + * A wallet that can be constructed from external data e.g utxos and an address. + * It doesn't allow you to sign transactions/messages. This needs to be handled separately. + */ +export class ExternalWallet implements AbstractWallet { + translucent: Translucent; + walletDetails: { + address: Address; + utxos?: UTxO[]; + rewardAddress?: RewardAddress; + }; + addressDetails: AddressDetails; + + constructor( + translucent: Translucent, + address: Address, + utxos?: UTxO[], + rewardAddress?: RewardAddress, + ) { + this.translucent = translucent; + this.walletDetails = { + address, + utxos, + rewardAddress, + }; + const addressDetails = this.translucent.utils.getAddressDetails(address); + this.addressDetails = addressDetails; + } + + async address(): Promise
{ + return this.walletDetails.address; + } + async rewardAddress(): Promise { + const rewardAddr = + !this.walletDetails.rewardAddress && this.addressDetails.stakeCredential + ? (() => + C.RewardAddress.new( + this.translucent.network === "Mainnet" ? 1 : 0, + toCore.credential(this.addressDetails.stakeCredential), + ) + .to_address() + .to_bech32(undefined))() + : this.walletDetails.rewardAddress; + return rewardAddr || null; + } + async getUtxos(): Promise { + return this.walletDetails.utxos + ? this.walletDetails.utxos + : await this.translucent.utxosAt( + paymentCredentialOf(this.walletDetails.address), + ); + } + async getUtxosCore(): Promise { + const coreUtxos = C.TransactionUnspentOutputs.new(); + (this.walletDetails.utxos + ? this.walletDetails.utxos + : await this.translucent.utxosAt( + paymentCredentialOf(this.walletDetails.address), + ) + ).forEach((utxo) => coreUtxos.add(utxoToCore(utxo))); + return coreUtxos; + } + async getDelegation(): Promise { + const rewardAddr = await this.rewardAddress(); + + return rewardAddr + ? await this.translucent.delegationAt(rewardAddr) + : { poolId: null, rewards: 0n }; + } + async signTx(): Promise { + throw new Error("Not implemented"); + } + async signMessage(): Promise { + throw new Error("Not implemented"); + } + async submitTx(tx: Transaction): Promise { + return await this.translucent.provider.submitTx(tx); + } +} diff --git a/src/wallets/seed.ts b/src/wallets/seed.ts new file mode 100644 index 0000000..265f661 --- /dev/null +++ b/src/wallets/seed.ts @@ -0,0 +1,136 @@ +import { signData } from "../misc/sign_data"; +import { discoverOwnUsedTxKeyHashes, walletFromSeed } from "../misc/wallet"; +import { + Address, + C, + Delegation, + KeyHash, + Payload, + RewardAddress, + SignedMessage, + Transaction, + Translucent, + TxHash, + UTxO, + paymentCredentialOf, + utxoToCore, +} from "../mod"; +import { AbstractWallet } from "./abstract"; + +/** + * 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. + */ +export class SeedWallet implements AbstractWallet { + translucent: Translucent; + private address_: string; + private rewardAddress_: string | null; + private paymentKey: string; + private stakeKey: string | null; + private paymentKeyHash: string; + private stakeKeyHash: string; + private privKeyHashMap: Record; + constructor( + translucent: Translucent, + seed: string, + options?: { + addressType?: "Base" | "Enterprise"; + accountIndex?: number; + password?: string; + }, + ) { + this.translucent = translucent; + const { address, rewardAddress, paymentKey, stakeKey } = walletFromSeed( + seed, + { + addressType: options?.addressType || "Base", + accountIndex: options?.accountIndex || 0, + password: options?.password, + network: this.translucent.network, + }, + ); + this.address_ = address; + this.rewardAddress_ = rewardAddress; + this.paymentKey = paymentKey; + this.stakeKey = stakeKey; + const paymentKeyHash = C.PrivateKey.from_bech32(paymentKey) + .to_public() + .hash() + .to_hex(); + this.paymentKeyHash = paymentKeyHash; + const stakeKeyHash = stakeKey + ? C.PrivateKey.from_bech32(stakeKey).to_public().hash().to_hex() + : ""; + this.stakeKeyHash = stakeKeyHash; + const privKeyHashMap = { + [paymentKeyHash]: paymentKey, + [stakeKeyHash]: stakeKey, + }; + this.privKeyHashMap = privKeyHashMap; + } + + // deno-lint-ignore require-await + async address(): Promise
{ + return this.address_; + } + // deno-lint-ignore require-await + async rewardAddress(): Promise { + return this.rewardAddress_ || null; + } + // deno-lint-ignore require-await + async getUtxos(): Promise { + return this.translucent.utxosAt(paymentCredentialOf(this.address_)); + } + async getUtxosCore(): Promise { + const coreUtxos = C.TransactionUnspentOutputs.new(); + ( + await this.translucent.utxosAt(paymentCredentialOf(this.address_)) + ).forEach((utxo) => { + coreUtxos.add(utxoToCore(utxo)); + }); + return coreUtxos; + } + async getDelegation(): Promise { + const rewardAddr = await this.rewardAddress(); + return rewardAddr + ? await this.translucent.delegationAt(rewardAddr) + : { poolId: null, rewards: 0n }; + } + async signTx(tx: C.Transaction): Promise { + const utxos = await this.translucent.utxosAt(this.address_); + const ownKeyHashes: Array = [ + this.paymentKeyHash, + this.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(this.privKeyHashMap[keyHash]!), + ); + txWitnessSetBuilder.add_vkey(witness); + }); + return txWitnessSetBuilder.build(); + } + // deno-lint-ignore require-await + async signMessage( + address: Address | RewardAddress, + payload: Payload, + ): Promise { + const { + paymentCredential, + stakeCredential, + address: { hex: hexAddress }, + } = this.translucent.utils.getAddressDetails(address); + const keyHash = paymentCredential?.hash || stakeCredential?.hash; + const privateKey = this.privKeyHashMap[keyHash!]; + if (!privateKey) { + throw new Error(`Cannot sign message for address: ${address}.`); + } + return signData(hexAddress, payload, privateKey); + } + async submitTx(tx: Transaction): Promise { + return await this.translucent.provider.submitTx(tx); + } +} diff --git a/src/wallets/wallet_connector.ts b/src/wallets/wallet_connector.ts new file mode 100644 index 0000000..e16f081 --- /dev/null +++ b/src/wallets/wallet_connector.ts @@ -0,0 +1,87 @@ +import { + Address, + Delegation, + Payload, + RewardAddress, + SignedMessage, + Transaction, + TxHash, + UTxO, + WalletApi, + coreToUtxo, + fromHex, + toHex, + C, + Translucent, +} from "../mod"; +import { AbstractWallet } from "./abstract"; + +export class WalletConnector implements AbstractWallet { + translucent: Translucent; + api: WalletApi; + + constructor(translucent: Translucent, api: WalletApi) { + this.translucent = translucent; + this.api = api; + } + + async getAddressHex() { + const [addressHex] = await this.api.getUsedAddresses(); + if (addressHex) return addressHex; + + const [unusedAddressHex] = await this.api.getUnusedAddresses(); + return unusedAddressHex; + } + + async address(): Promise
{ + return C.Address.from_bytes(fromHex(await this.getAddressHex())).to_bech32( + undefined, + ); + } + async rewardAddress(): Promise { + const [rewardAddressHex] = await this.api.getRewardAddresses(); + const rewardAddress = rewardAddressHex + ? C.RewardAddress.from_address( + C.Address.from_bytes(fromHex(rewardAddressHex)), + )! + .to_address() + .to_bech32(undefined) + : null; + return rewardAddress; + } + async getUtxos(): Promise { + const utxos = ((await this.api.getUtxos()) || []).map((utxo) => { + const parsedUtxo = C.TransactionUnspentOutput.from_bytes(fromHex(utxo)); + return coreToUtxo(parsedUtxo); + }); + return utxos; + } + async getUtxosCore(): Promise { + const utxos = C.TransactionUnspentOutputs.new(); + ((await this.api.getUtxos()) || []).forEach((utxo) => { + utxos.add(C.TransactionUnspentOutput.from_bytes(fromHex(utxo))); + }); + return utxos; + } + async getDelegation(): Promise { + const rewardAddr = await this.rewardAddress(); + return rewardAddr + ? await this.translucent.delegationAt(rewardAddr) + : { poolId: null, rewards: 0n }; + } + async signTx(tx: C.Transaction): Promise { + const witnessSet = await this.api.signTx(toHex(tx.to_bytes()), true); + return C.TransactionWitnessSet.from_bytes(fromHex(witnessSet)); + } + async signMessage( + address: Address | RewardAddress, + payload: Payload, + ): Promise { + const hexAddress = toHex(C.Address.from_bech32(address).to_bytes()); + return await this.api.signData(hexAddress, payload); + } + async submitTx(tx: Transaction): Promise { + const txHash = await this.api.submitTx(tx); + return txHash; + } +} diff --git a/tests/mod.test.ts b/tests/mod.test.ts index 9074aa9..6e6cbf4 100644 --- a/tests/mod.test.ts +++ b/tests/mod.test.ts @@ -109,13 +109,12 @@ it("Wallet from utxos roundtrip (legacy utxos)", async () => { "8282582040d1e106e587e6123aef50fa4347e34ec8c76f405ee9802ce90a345077b4e264018258390093de07f8feeda49b93c4cfd6817ebcfb7bfc4a0b18237c0196b205da3446b33ae55d27691cbbed080df5182262c307cdab62d3a4883f39df821a009a29bab82d581c126b8676446c84a5cd6e3259223b16a2314c5676b88ae1c1f8579a8fa144744d494e199d72581c3044e869070c8acf8f0ee6c87c285008fde6990f956a07cb9fd1e070a1444e616d6901581c34d1adbf3a7e95b253fd0999fb85e2d41d4121b36b834b83ac069ebba144414749581903ef581c370e055e07af274b588dc9bf3ef3203ec8b6830b7cdfaca9de38fc8fa146506c616e657401581c4122bca203030a36d8b605ab488335b1f2ee40dd7cd1454ce42f545ea143666c7901581c416e45fb08dc61a59a6bea82bde19e3014cef27ac4616c4f583adc15a144436f6f6c01581c4371ba767200e3e50e5dc1ff17f3f8d5154fa606534b03251a58e273a146436f6c6f727301581c46eea81aeed38d5e3595c4bf148a60f28006332ea9721a55748758f4a148456c657068616e7401581c54c1878edf4e6d49eab5b47b95c1bb9c6cf0622bf8dc5df1de6dc51fa1426a6f01581c55b2100ca1ac2f6ad955c5cc8c1c87d0f6155fd6ccb6857b7da5754ca145746f75636801581c57fca08abbaddee36da742a839f7d83a7e1d2419f1507fcbf3916522a54443484f431a0096dfcb444d494e541b0000000129f4a1764556414e494c1a013373b6465242455252591a3b9ac9d4465342455252591a3b9ac985581c5b7e2b5608c0f38eb186241f8b883d2e7bcad382f78c1e4e8993e513a14763616c76696e3101581c5c1c784a488bde15f9519142a9ee07886266598052f3dcc2105a6329a146526f636b657401581c60614c06bc93079c4e884db4dd966d2912aaae32a07a4506f749426ca1445472656501581c64834db34a8719177ee5ce1eda1c4cb91d2dca28bfc536073ba2176aa1445761766501581c648fd48f82c0a0687fb3d48178d0bcb3f0c5fe6cec68a1385129d4faa1444e616d6901581c68fdca4411f9a96df98e5517db3b49be01afc306655bf5e17ace78aea1477363656e65727901581c6998f4f9478186f3c182a4b334ef4e7d440658af06562ace55f74587a1444475636b01581c715b818224e0f83c747e8ab443a647ccf4dd2a6f0b2b37d7af8b3a60a149427574746572666c7901581c7509d2c7f0f3eb36d0336fcb7127bf3db1d5336583908d049e5a3716a1444e616d6901581c75ac2ca896ba71faee950eff23ad269b8dbd7c66e0806164ae74352ca14b526f636b6574416761696e01581c766bb5e84dc3438a5d7b2d686da04047f5c3ea981eac03de63071444a145576174657201581c83e1588347feaa9183030a82aa301b6a60b97c3844cdfd56b4bde133a144526f736501581c844c30a27b7756379ddae50e0dbe86c001d1a576d7adfdccc42c0b0da1444e616d6901581c868c245e1d99b32509542230fe744c120358353f195b6c19845134aca151526f636b6574202121202320242066736401581c890b7c122f3383fc859eecec99fa0ced3521b6e96f521a37de49e8baa1414b01581c9733e6e8f97a60b3a5c1ce6fe82f5c75b030766330bc5b4ea92b8b07a14a416e6f746865724170651842581c9d6eed0860acf0020af0d1f42c4c5eca26a2fb65aa09ed06bfe8d6ffa146666f7265737401581c9e6e63bbd52f0106716c6c920dcc9ad716ac9b001a18259eb20948e2a144436f6f6c01581c9ee64af489f0f8c6b64524988397b17269137d69830ae36c8b1530eba1414101581ca302467c86dfa2ae00e391745ba2de255ea2ec6f634e97a0a8c8bf27a1445374617201581ca380eb65f09337b3a562647823bc7edc9200f1fb8ff3bf1007263b69a1444a4f4f4f01581cbef61059036b3a7dbdb881d8239994f481d521c52908ad4c07421ce0a145506172697301581cc0355288172685f5732362b2bb4b8524223ad80b9cd97510b1460ef0a1414601581cc2f3a2c1af2d8cf25a89c4ed6c5c3bec4a4881341722ff42a31bf8cca1444d6f6f6e01581cc316c4d73ecb737ec21145a2dab42b4ad0dd5409118b482d7c601861a1445465737401581cc5d47df9211545b6b3bcb72e16654b1e46c9151fddbbe87ca0569e2ea14753636f6f74657201581cc921e0caf5ea1d4e313c96a4b416a91e085ba1355d9cd51e1d1e51f0a1414a01581ccf997119d2bf1fcbf73da2648e7eb047d4d0c5d8da04a426f46ee1a3a14472696e6701581cd311d3488cc4fef19d05634adce8534977a3bc6fc18136ad65df1d4fa1446c7120041a00b5b5b9581cd3840433dfd5720677fbcd3a4e14f44b249e3327fe1b8821f549bfd0a14349636501581ce3f3eb3f4f343fae935cea3a95f13cfba4eb7262919aec4b6d497d3aa1414901581ce7273c2510d45ec13847d2371a5289b2e3c132af29a1f8571bc15625a14f526f636b65744c61756e636831323301581cea15a43c13f242786c69c2ad448739430a666d66adf19c1701b1cbefa1414b01581cf334fced32a07a832b57d8795994e59e49c3d7dd8809748ff2eb3f0ea1434a4f4f01", "82825820109fbbedaa6d8c0ed0262411802ddaf9e8596396cb9e010248f52df65a7d0d87018258390093de07f8feeda49b93c4cfd6817ebcfb7bfc4a0b18237c0196b205da3446b33ae55d27691cbbed080df5182262c307cdab62d3a4883f39df1a1f069542", ]; - translucent.selectWalletFrom({ - address: - "addr_test1qzfauplclmk6fxuncn8adqt7hnahhlz2pvvzxlqpj6eqtk35g6en4e2aya53ewldpqxl2xpzvtps0ndtvtf6fzpl880srm02gc", - utxos: rawUtxos.map((raw) => + translucent.selectWalletFrom( + "addr_test1qzfauplclmk6fxuncn8adqt7hnahhlz2pvvzxlqpj6eqtk35g6en4e2aya53ewldpqxl2xpzvtps0ndtvtf6fzpl880srm02gc", + rawUtxos.map((raw) => coreToUtxo(C.TransactionUnspentOutput.from_bytes(fromHex(raw))), ), - }); + ); const rawUtxos_ = (await translucent.wallet.getUtxos()).map((utxo) => toHex(utxoToCore(utxo).to_bytes()), @@ -383,10 +382,9 @@ it("toUnit/fromUnit property test", () => { }); it("Preserve task/transaction order", async () => { - translucent.selectWalletFrom({ - address: - "addr_test1qq90qrxyw5qtkex0l7mc86xy9a6xkn5t3fcwm6wq33c38t8nhh356yzp7k3qwmhe4fk0g5u6kx5ka4rz5qcq4j7mvh2sts2cfa", - utxos: [ + translucent.selectWalletFrom( + "addr_test1qq90qrxyw5qtkex0l7mc86xy9a6xkn5t3fcwm6wq33c38t8nhh356yzp7k3qwmhe4fk0g5u6kx5ka4rz5qcq4j7mvh2sts2cfa", + [ { txHash: "2eefc93bc0dda80e78890f1f965733239e1f64f76555e8dcde1a4aa7db67b129", @@ -399,7 +397,7 @@ it("Preserve task/transaction order", async () => { scriptRef: null, }, ], - }); + ); const txCompA = translucent .newTx()