From 041b24e4135fb52c54c3ad061fe5475b6bb03551 Mon Sep 17 00:00:00 2001 From: Dudi Edri Date: Thu, 4 May 2023 00:09:15 +0300 Subject: [PATCH 1/5] Add Koios Provider to Lucid #102 --- package.json | 5 +- src/provider/koios.ts | 236 ++++++++++++++++++++++++++++++++++++++++++ src/provider/mod.ts | 1 + tests/koios.test.ts | 95 +++++++++++++++++ 4 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 src/provider/koios.ts create mode 100644 tests/koios.test.ts diff --git a/package.json b/package.json index 9f9b54e4..6fc200ad 100644 --- a/package.json +++ b/package.json @@ -4,5 +4,8 @@ "license": "MIT", "author": "Alessandro Konrad", "description": "Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js.", - "repository": "https://github.com/spacebudz/lucid" + "repository": "https://github.com/spacebudz/lucid", + "dependencies": { + "@adabox/koios-ts-client": "^1.0.6" + } } diff --git a/src/provider/koios.ts b/src/provider/koios.ts new file mode 100644 index 00000000..8cefe410 --- /dev/null +++ b/src/provider/koios.ts @@ -0,0 +1,236 @@ +import { + Address, + Assets, + Credential, + Datum, + DatumHash, + Delegation, + Network, + OutRef, + ProtocolParameters, + Provider, + RewardAddress, + Transaction, + TxHash, + Unit, + UTxO, +} from "../types/mod.ts"; +import {BackendFactory, KoiosHttpError, KoiosTimeoutError} from "@adabox/koios-ts-client/dist/index.js" +import {C} from "../core/core.ts"; +import {applyDoubleCborEncoding, fromHex, fromUnit} from "../utils/utils.ts"; + +export class KoiosProvider implements Provider { + + private readonly backendService + + constructor(network: Network) { + if (network === 'Mainnet') { + this.backendService = BackendFactory.getKoiosMainnetService() + } else if (network === 'Preview') { + this.backendService = BackendFactory.getKoiosPreviewService() + } else if (network === 'Preprod') { + this.backendService = BackendFactory.getKoiosPreprodService() + } else { + throw Error("Unsupported Network Type") + } + } + + async getProtocolParameters(): Promise { + const result = await this.backendService.getEpochService().getEpochProtocolParameters() + + return { + minFeeA: parseInt(result[0].min_fee_a), + minFeeB: parseInt(result[0].min_fee_b), + maxTxSize: parseInt(result[0].max_tx_size), + maxValSize: parseInt(result[0].max_val_size), + keyDeposit: BigInt(result[0].key_deposit), + poolDeposit: BigInt(result[0].pool_deposit), + priceMem: parseFloat(result[0].price_mem), + priceStep: parseFloat(result[0].price_step), + maxTxExMem: BigInt(result[0].max_tx_ex_mem), + maxTxExSteps: BigInt(result[0].max_tx_ex_steps), + coinsPerUtxoByte: BigInt(result[0].coins_per_utxo_size), + collateralPercentage: parseInt(result[0].collateral_percent), + maxCollateralInputs: parseInt(result[0].max_collateral_inputs), + costModels: JSON.parse(result[0].cost_models), + }; + } + + async getUtxos(addressOrCredential: Address | Credential): Promise { + const queryPredicate = (() => { + if (typeof addressOrCredential === "string") return addressOrCredential; + // should be 'script' (CIP-0005) + return addressOrCredential.type === "Key" + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh") + : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh"); + })(); + try { + const result = await this.backendService.getAddressService().getAddressInformation([queryPredicate]) + if (Array.isArray(result) && result.length > 0 && result[0].utxo_set && result[0].utxo_set.length > 0) { + return this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address) + } else { + return [] + } + } catch (e) { + throw new Error("Could not fetch UTxOs from Koios. Try again."); + } + } + + private async koiosUtxosToUtxos(result: any, address?: string): Promise { + return (await Promise.all( + result.map(async (r: any) => ({ + txHash: r.tx_hash, + outputIndex: r.tx_index, + assets: (() => { + const a: Assets = {}; + r.asset_list.forEach((am: any) => { + a[am.policy_id + am.asset_name] = BigInt(am.quantity); + }); + return a; + })(), + address: address ? address : r.payment_addr.bech32, + datumHash: !r.inline_datum ? r.datum_hash : undefined, + datum: r.inline_datum, + scriptRef: { + type: r.reference_script ? r.reference_script.type : null, + script: r.reference_script ? applyDoubleCborEncoding(r.reference_script.bytes) : null + }, + })), + )) as UTxO[]; + } + + async getUtxosWithUnit(addressOrCredential: Address | Credential, unit: Unit): Promise { + const queryPredicate = (() => { + if (typeof addressOrCredential === "string") return addressOrCredential; + // should be 'script' (CIP-0005) + return addressOrCredential.type === "Key" + ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh") + : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh"); + })(); + try { + const result = await this.backendService.getAddressService().getAddressInformation([queryPredicate]) + if (Array.isArray(result) && result.length > 0 && result[0].utxo_set && result[0].utxo_set.length > 0) { + return (await this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address)).filter((utxo): utxo is UTxO => { + const keys = Object.keys(utxo.assets) + return keys.length > 0 && keys.includes(unit) + }) + } else { + return [] + } + } catch (e) { + throw new Error("Could not fetch UTxOs from Koios. Try again."); + } + } + + async getUtxoByUnit(unit: Unit): Promise { + let assetAddresses + try { + let { policyId, assetName } = fromUnit(unit) + assetName = String(assetName) + assetAddresses = await this.backendService.getAssetService().getAssetAddresses(policyId, assetName) + } catch (e) { + throw new Error("Could not fetch UTxO from Koios. Try again."); + } + if (Array.isArray(assetAddresses) && assetAddresses.length > 0) { + if (assetAddresses.length > 1) { + throw new Error("Unit needs to be an NFT or only held by one address."); + } + const address = assetAddresses[0].payment_address + try { + const utxos: UTxO[] = await this.getUtxos(address) + const result = utxos.find((utxo): utxo is UTxO => { + const keys = Object.keys(utxo.assets) + return keys.length > 0 && keys.includes(unit) + }) + if (result) { + return result + } + } catch (e) { + throw new Error("Could not fetch UTxO from Koios. Try again."); + } + } + throw new Error("Unit not found."); + } + + async getUtxosByOutRef(outRefs: OutRef[]): Promise { + try { + const utxos = [] + const queryHashes = [...new Set(outRefs.map((outRef) => outRef.txHash))]; + const result = await this.backendService.getTransactionsService().getTransactionUTxOs(queryHashes) + if (Array.isArray(result) && result.length > 0) { + for (const utxo of result) { + if (utxo.outputs && utxo.outputs.length > 0) { + utxos.push(await this.koiosUtxosToUtxos(utxo.outputs)) + } + } + return utxos.reduce((acc, utxos) => acc.concat(utxos), []).filter((utxo) => + outRefs.some((outRef) => + utxo.txHash === outRef.txHash && utxo.outputIndex === outRef.outputIndex + ) + ); + } else { + return [] + } + } catch (e) { + throw new Error("Could not fetch UTxOs from Koios. Try again."); + } + } + + async getDelegation(rewardAddress: RewardAddress): Promise { + try { + const result = await this.backendService.getAccountService().getAccountInformation([rewardAddress]) + if (Array.isArray(result) && result.length > 0) { + return { + poolId: result[0].delegated_pool || null, + rewards: BigInt(result[0].rewards_available), + } + } + } catch (e) { + throw new Error("Could not fetch Account Information from Koios. Try again."); + } + throw new Error("No Delegation Found by Reward Address"); + } + + async getDatum(datumHash: DatumHash): Promise { + try { + const result = await this.backendService.getScriptService().getDatumInformation([datumHash]) + if (Array.isArray(result) && result.length > 0) { + return result[0].bytes + } + } catch (e) { + throw new Error("Could not fetch Datum Information from Koios. Try again."); + } + throw new Error("No Datum Found by Datum Hash"); + } + + awaitTx(txHash: TxHash, checkInterval = 3000): Promise { + return new Promise((res) => { + const confirmation = setInterval(async () => { + try { + const result = await this.backendService.getTransactionsService().getTransactionInformation([txHash]) + if (Array.isArray(result) && result.length > 0) { + clearInterval(confirmation); + await new Promise((res) => setTimeout(() => res(1), 1000)); + return res(true) + } + } catch (e) { + throw new Error("Could not fetch Datum Information from Koios. Try again."); + } + }, checkInterval); + }); + } + + async submitTx(tx: Transaction): Promise { + try { + return await this.backendService.getTransactionsService().submitTransaction(fromHex(tx)) + } catch (e) { + if (e instanceof KoiosHttpError) { + throw new Error(`Transaction Submission Error: ${e.message}`); + } else if (e instanceof KoiosTimeoutError) { + throw new Error("Timeout Error."); + } else { + throw new Error("Could not submit transaction."); + } + } + } +} \ No newline at end of file diff --git a/src/provider/mod.ts b/src/provider/mod.ts index 73e36238..f8d8ec87 100644 --- a/src/provider/mod.ts +++ b/src/provider/mod.ts @@ -1,3 +1,4 @@ export * from "./blockfrost.ts"; export * from "./kupmios.ts"; export * from "./emulator.ts"; +export * from "./koios.ts"; diff --git a/tests/koios.test.ts b/tests/koios.test.ts new file mode 100644 index 00000000..655f5da3 --- /dev/null +++ b/tests/koios.test.ts @@ -0,0 +1,95 @@ +import {KoiosProvider} from "../src/provider/koios.ts"; +import {Datum, Delegation, ProtocolParameters, UTxO} from "../src/types/types.ts"; +import {assert} from "https://deno.land/std@0.145.0/testing/asserts.ts"; + +Deno.test("getProtocolParameters", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const pp: ProtocolParameters = await koios.getProtocolParameters(); + assert(pp); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getUtxos", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const utxos: UTxO[] = await koios.getUtxos("addr1qy2jt0qpqz2z2z9zx5w4xemekkce7yderz53kjue53lpqv90lkfa9sgrfjuz6uvt4uqtrqhl2kj0a9lnr9ndzutx32gqleeckv"); + assert(utxos); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getUtxosWithUnit", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const utxos: UTxO[] = await koios.getUtxosWithUnit( + "addr1q8vaadv0h7atv366u6966u4rft2svjlf5uajy8lkpsgdrc24rnskuetxz2u3m5ac22s3njvftxcl2fc8k8kjr088ge0qpn6xhn", + "85152e10643c1440ba2ba817e3dd1faf7bd7296a8b605efd0f0f2d1844696d656e73696f6e426f78202330313739"); + assert(utxos); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getUtxoByUnit", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const utxo: UTxO = await koios.getUtxoByUnit("85152e10643c1440ba2ba817e3dd1faf7bd7296a8b605efd0f0f2d1844696d656e73696f6e426f78202330313739"); + assert(utxo); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getUtxosByOutRef", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const utxos: UTxO[] = await koios.getUtxosByOutRef([{txHash: 'c6ee20549eab1e565a4bed119bb8c7fc2d11cc5ea5e1e25433a34f0175c0bef6', outputIndex: 0}]); + assert(utxos); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getDelegation", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const delegation: Delegation = await koios.getDelegation('stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250'); + assert(delegation); + } catch (e) { + console.log(e) + } +}); + +Deno.test("getDatum", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const datum: Datum = await koios.getDatum('818ee3db3bbbd04f9f2ce21778cac3ac605802a4fcb00c8b3a58ee2dafc17d46'); + assert(datum); + } catch (e) { + console.log(e) + } +}); + +Deno.test("awaitTx", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const isConfirmed: boolean = await koios.awaitTx('f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e'); + assert(isConfirmed); + } catch (e) { + console.log(e) + } +}); + +Deno.test("submitTxBadRequest", async () => { + const koios = new KoiosProvider("Mainnet") + try { + const txId: string = await koios.submitTx('80'); + assert(txId); + } catch (e) { + console.log(e) + } +}); From 1766fdd321d2499bc898a0a43b267e08568a59c9 Mon Sep 17 00:00:00 2001 From: Dudi Edri Date: Thu, 4 May 2023 13:50:30 +0300 Subject: [PATCH 2/5] Add Koios Provider to Lucid - PR Feedback + Add to Docs --- docs/docs/getting-started/choose-provider.md | 6 +++ docs/index.yml | 5 +++ package.json | 5 +-- src/provider/koios.ts | 41 +++++++------------- tests/koios.test.ts | 28 +++++++------ 5 files changed, 42 insertions(+), 43 deletions(-) diff --git a/docs/docs/getting-started/choose-provider.md b/docs/docs/getting-started/choose-provider.md index b49e669f..41b7117b 100644 --- a/docs/docs/getting-started/choose-provider.md +++ b/docs/docs/getting-started/choose-provider.md @@ -21,6 +21,12 @@ const lucid = await Lucid.new( ); ``` +```js +import { Lucid, Koios } from "https://deno.land/x/lucid/mod.ts"; + +const lucid = await Lucid.new(new Koios("Preprod"), "Preprod"); +``` + ### Kupmios Kupmios is a mix of [Ogmios](https://ogmios.dev/) and [Kupo](https://cardanosolutions.github.io/kupo/). diff --git a/docs/index.yml b/docs/index.yml index e278d326..930d355e 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -135,6 +135,11 @@ usage: ), ); ``` + ```js { title="koios.js" } + import { Lucid, Koios } from "https://deno.land/x/lucid/mod.ts" + + const lucid = await Lucid.new(new Koios("Preprod"), "Preprod"); + ``` ```js { title="kupmios.js" } import { Lucid, Kupmios } from "https://deno.land/x/lucid/mod.ts" diff --git a/package.json b/package.json index 6fc200ad..9f9b54e4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,5 @@ "license": "MIT", "author": "Alessandro Konrad", "description": "Lucid is a library, which allows you to create Cardano transactions and off-chain code for your Plutus contracts in JavaScript, Deno and Node.js.", - "repository": "https://github.com/spacebudz/lucid", - "dependencies": { - "@adabox/koios-ts-client": "^1.0.6" - } + "repository": "https://github.com/spacebudz/lucid" } diff --git a/src/provider/koios.ts b/src/provider/koios.ts index 8cefe410..78e32836 100644 --- a/src/provider/koios.ts +++ b/src/provider/koios.ts @@ -15,11 +15,10 @@ import { Unit, UTxO, } from "../types/mod.ts"; -import {BackendFactory, KoiosHttpError, KoiosTimeoutError} from "@adabox/koios-ts-client/dist/index.js" -import {C} from "../core/core.ts"; +import {BackendFactory, KoiosHttpError, KoiosTimeoutError} from "https://esm.sh/@adabox/koios-ts-client@1.0.6/dist/index.js" import {applyDoubleCborEncoding, fromHex, fromUnit} from "../utils/utils.ts"; -export class KoiosProvider implements Provider { +export class Koios implements Provider { private readonly backendService @@ -57,22 +56,15 @@ export class KoiosProvider implements Provider { } async getUtxos(addressOrCredential: Address | Credential): Promise { - const queryPredicate = (() => { - if (typeof addressOrCredential === "string") return addressOrCredential; - // should be 'script' (CIP-0005) - return addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh") - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh"); - })(); - try { - const result = await this.backendService.getAddressService().getAddressInformation([queryPredicate]) + if (typeof addressOrCredential === "string") { + const result = await this.backendService.getAddressService().getAddressInformation([addressOrCredential]); if (Array.isArray(result) && result.length > 0 && result[0].utxo_set && result[0].utxo_set.length > 0) { - return this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address) + return (await this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address)) } else { return [] } - } catch (e) { - throw new Error("Could not fetch UTxOs from Koios. Try again."); + } else { + throw Error('getUtxos by Credential Type is not supported in Koios yet.') } } @@ -100,25 +92,18 @@ export class KoiosProvider implements Provider { } async getUtxosWithUnit(addressOrCredential: Address | Credential, unit: Unit): Promise { - const queryPredicate = (() => { - if (typeof addressOrCredential === "string") return addressOrCredential; - // should be 'script' (CIP-0005) - return addressOrCredential.type === "Key" - ? C.Ed25519KeyHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh") - : C.ScriptHash.from_hex(addressOrCredential.hash).to_bech32("addr_vkh"); - })(); - try { - const result = await this.backendService.getAddressService().getAddressInformation([queryPredicate]) - if (Array.isArray(result) && result.length > 0 && result[0].utxo_set && result[0].utxo_set.length > 0) { - return (await this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address)).filter((utxo): utxo is UTxO => { + if (typeof addressOrCredential === "string") { + const utxos = await this.getUtxos(addressOrCredential); + if (utxos && utxos.length > 0) { + return utxos.filter((utxo): utxo is UTxO => { const keys = Object.keys(utxo.assets) return keys.length > 0 && keys.includes(unit) }) } else { return [] } - } catch (e) { - throw new Error("Could not fetch UTxOs from Koios. Try again."); + } else { + throw Error('getUtxosWithUnit by Credential Type is not supported in Koios yet.') } } diff --git a/tests/koios.test.ts b/tests/koios.test.ts index 655f5da3..ccbec721 100644 --- a/tests/koios.test.ts +++ b/tests/koios.test.ts @@ -1,9 +1,10 @@ -import {KoiosProvider} from "../src/provider/koios.ts"; +import {Koios} from "../src/provider/koios.ts"; import {Datum, Delegation, ProtocolParameters, UTxO} from "../src/types/types.ts"; import {assert} from "https://deno.land/std@0.145.0/testing/asserts.ts"; +import {Lucid} from "../src/lucid/lucid.ts"; Deno.test("getProtocolParameters", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const pp: ProtocolParameters = await koios.getProtocolParameters(); assert(pp); @@ -13,7 +14,7 @@ Deno.test("getProtocolParameters", async () => { }); Deno.test("getUtxos", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxos("addr1qy2jt0qpqz2z2z9zx5w4xemekkce7yderz53kjue53lpqv90lkfa9sgrfjuz6uvt4uqtrqhl2kj0a9lnr9ndzutx32gqleeckv"); assert(utxos); @@ -23,11 +24,12 @@ Deno.test("getUtxos", async () => { }); Deno.test("getUtxosWithUnit", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxosWithUnit( "addr1q8vaadv0h7atv366u6966u4rft2svjlf5uajy8lkpsgdrc24rnskuetxz2u3m5ac22s3njvftxcl2fc8k8kjr088ge0qpn6xhn", "85152e10643c1440ba2ba817e3dd1faf7bd7296a8b605efd0f0f2d1844696d656e73696f6e426f78202330313739"); + console.log(utxos) assert(utxos); } catch (e) { console.log(e) @@ -35,7 +37,7 @@ Deno.test("getUtxosWithUnit", async () => { }); Deno.test("getUtxoByUnit", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const utxo: UTxO = await koios.getUtxoByUnit("85152e10643c1440ba2ba817e3dd1faf7bd7296a8b605efd0f0f2d1844696d656e73696f6e426f78202330313739"); assert(utxo); @@ -45,7 +47,7 @@ Deno.test("getUtxoByUnit", async () => { }); Deno.test("getUtxosByOutRef", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxosByOutRef([{txHash: 'c6ee20549eab1e565a4bed119bb8c7fc2d11cc5ea5e1e25433a34f0175c0bef6', outputIndex: 0}]); assert(utxos); @@ -55,9 +57,13 @@ Deno.test("getUtxosByOutRef", async () => { }); Deno.test("getDelegation", async () => { - const koios = new KoiosProvider("Mainnet") + const lucid = await Lucid.new( + new Koios("Mainnet"), + "Mainnet" + ); + assert(lucid) try { - const delegation: Delegation = await koios.getDelegation('stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250'); + const delegation: Delegation = await lucid.delegationAt('stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250'); assert(delegation); } catch (e) { console.log(e) @@ -65,7 +71,7 @@ Deno.test("getDelegation", async () => { }); Deno.test("getDatum", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const datum: Datum = await koios.getDatum('818ee3db3bbbd04f9f2ce21778cac3ac605802a4fcb00c8b3a58ee2dafc17d46'); assert(datum); @@ -75,7 +81,7 @@ Deno.test("getDatum", async () => { }); Deno.test("awaitTx", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const isConfirmed: boolean = await koios.awaitTx('f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e'); assert(isConfirmed); @@ -85,7 +91,7 @@ Deno.test("awaitTx", async () => { }); Deno.test("submitTxBadRequest", async () => { - const koios = new KoiosProvider("Mainnet") + const koios = new Koios("Mainnet") try { const txId: string = await koios.submitTx('80'); assert(txId); From 27e6141d584a2260186788ad03d1093cc749a600 Mon Sep 17 00:00:00 2001 From: Dudi Edri Date: Sun, 3 Sep 2023 15:53:40 +0300 Subject: [PATCH 3/5] Add Koios Provider to Lucid --- src/provider/koios.ts | 103 ++++++++++++++++++++++++++++-------------- tests/koios.test.ts | 15 ++---- 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/src/provider/koios.ts b/src/provider/koios.ts index 78e32836..c2d0ea57 100644 --- a/src/provider/koios.ts +++ b/src/provider/koios.ts @@ -1,3 +1,4 @@ +import {applyDoubleCborEncoding, fromHex, fromUnit} from "../utils/utils.ts"; import { Address, Assets, @@ -5,7 +6,6 @@ import { Datum, DatumHash, Delegation, - Network, OutRef, ProtocolParameters, Provider, @@ -15,27 +15,17 @@ import { Unit, UTxO, } from "../types/mod.ts"; -import {BackendFactory, KoiosHttpError, KoiosTimeoutError} from "https://esm.sh/@adabox/koios-ts-client@1.0.6/dist/index.js" -import {applyDoubleCborEncoding, fromHex, fromUnit} from "../utils/utils.ts"; export class Koios implements Provider { - private readonly backendService + private readonly baseUrl: string - constructor(network: Network) { - if (network === 'Mainnet') { - this.backendService = BackendFactory.getKoiosMainnetService() - } else if (network === 'Preview') { - this.backendService = BackendFactory.getKoiosPreviewService() - } else if (network === 'Preprod') { - this.backendService = BackendFactory.getKoiosPreprodService() - } else { - throw Error("Unsupported Network Type") - } + constructor(baseUrl: string) { + this.baseUrl = baseUrl } async getProtocolParameters(): Promise { - const result = await this.backendService.getEpochService().getEpochProtocolParameters() + const result = await fetch(`${this.baseUrl}/epoch_params?limit=1`).then((res) => res.json()); return { minFeeA: parseInt(result[0].min_fee_a), @@ -57,7 +47,16 @@ export class Koios implements Provider { async getUtxos(addressOrCredential: Address | Credential): Promise { if (typeof addressOrCredential === "string") { - const result = await this.backendService.getAddressService().getAddressInformation([addressOrCredential]); + const body: any = {} + body['_addresses'] = [addressOrCredential] + const result = await fetch(`${this.baseUrl}/address_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); if (Array.isArray(result) && result.length > 0 && result[0].utxo_set && result[0].utxo_set.length > 0) { return (await this.koiosUtxosToUtxos(result[0].utxo_set, result[0].address)) } else { @@ -110,9 +109,10 @@ export class Koios implements Provider { async getUtxoByUnit(unit: Unit): Promise { let assetAddresses try { - let { policyId, assetName } = fromUnit(unit) + let {policyId, assetName} = fromUnit(unit) assetName = String(assetName) - assetAddresses = await this.backendService.getAssetService().getAssetAddresses(policyId, assetName) + assetAddresses = await fetch(`${this.baseUrl}/asset_addresses?_asset_policy=${policyId}&_asset_name=${assetName}`) + .then((res: Response) => res.json()); } catch (e) { throw new Error("Could not fetch UTxO from Koios. Try again."); } @@ -140,8 +140,16 @@ export class Koios implements Provider { async getUtxosByOutRef(outRefs: OutRef[]): Promise { try { const utxos = [] - const queryHashes = [...new Set(outRefs.map((outRef) => outRef.txHash))]; - const result = await this.backendService.getTransactionsService().getTransactionUTxOs(queryHashes) + const body: any = {} + body['_tx_hashes'] = [...new Set(outRefs.map((outRef) => outRef.txHash))]; + const result = await fetch(`${this.baseUrl}/tx_utxos`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); if (Array.isArray(result) && result.length > 0) { for (const utxo of result) { if (utxo.outputs && utxo.outputs.length > 0) { @@ -163,7 +171,16 @@ export class Koios implements Provider { async getDelegation(rewardAddress: RewardAddress): Promise { try { - const result = await this.backendService.getAccountService().getAccountInformation([rewardAddress]) + const body: any = {} + body['_stake_addresses'] = [rewardAddress]; + const result = await fetch(`${this.baseUrl}/account_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); if (Array.isArray(result) && result.length > 0) { return { poolId: result[0].delegated_pool || null, @@ -178,7 +195,16 @@ export class Koios implements Provider { async getDatum(datumHash: DatumHash): Promise { try { - const result = await this.backendService.getScriptService().getDatumInformation([datumHash]) + const body: any = {} + body['_datum_hashes'] = [datumHash]; + const result = await fetch(`${this.baseUrl}/datum_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); if (Array.isArray(result) && result.length > 0) { return result[0].bytes } @@ -192,7 +218,16 @@ export class Koios implements Provider { return new Promise((res) => { const confirmation = setInterval(async () => { try { - const result = await this.backendService.getTransactionsService().getTransactionInformation([txHash]) + const body: any = {} + body['_tx_hashes'] = [txHash]; + const result = await fetch(`${this.baseUrl}/tx_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); if (Array.isArray(result) && result.length > 0) { clearInterval(confirmation); await new Promise((res) => setTimeout(() => res(1), 1000)); @@ -206,16 +241,18 @@ export class Koios implements Provider { } async submitTx(tx: Transaction): Promise { - try { - return await this.backendService.getTransactionsService().submitTransaction(fromHex(tx)) - } catch (e) { - if (e instanceof KoiosHttpError) { - throw new Error(`Transaction Submission Error: ${e.message}`); - } else if (e instanceof KoiosTimeoutError) { - throw new Error("Timeout Error."); - } else { - throw new Error("Could not submit transaction."); - } + const result = await fetch(`${this.baseUrl}/tx_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/cbor' + }, + method: "POST", + body: fromHex(tx) + }).then((res: Response) => res.json()) + if (!result || result.error) { + if (result?.status_code === 400) throw new Error(result.message); + else throw new Error("Could not submit transaction."); } + return result } } \ No newline at end of file diff --git a/tests/koios.test.ts b/tests/koios.test.ts index ccbec721..f4307aad 100644 --- a/tests/koios.test.ts +++ b/tests/koios.test.ts @@ -3,8 +3,9 @@ import {Datum, Delegation, ProtocolParameters, UTxO} from "../src/types/types.ts import {assert} from "https://deno.land/std@0.145.0/testing/asserts.ts"; import {Lucid} from "../src/lucid/lucid.ts"; +const koios = new Koios("https://api.koios.rest/api/v0") + Deno.test("getProtocolParameters", async () => { - const koios = new Koios("Mainnet") try { const pp: ProtocolParameters = await koios.getProtocolParameters(); assert(pp); @@ -14,7 +15,6 @@ Deno.test("getProtocolParameters", async () => { }); Deno.test("getUtxos", async () => { - const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxos("addr1qy2jt0qpqz2z2z9zx5w4xemekkce7yderz53kjue53lpqv90lkfa9sgrfjuz6uvt4uqtrqhl2kj0a9lnr9ndzutx32gqleeckv"); assert(utxos); @@ -24,7 +24,6 @@ Deno.test("getUtxos", async () => { }); Deno.test("getUtxosWithUnit", async () => { - const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxosWithUnit( "addr1q8vaadv0h7atv366u6966u4rft2svjlf5uajy8lkpsgdrc24rnskuetxz2u3m5ac22s3njvftxcl2fc8k8kjr088ge0qpn6xhn", @@ -37,7 +36,6 @@ Deno.test("getUtxosWithUnit", async () => { }); Deno.test("getUtxoByUnit", async () => { - const koios = new Koios("Mainnet") try { const utxo: UTxO = await koios.getUtxoByUnit("85152e10643c1440ba2ba817e3dd1faf7bd7296a8b605efd0f0f2d1844696d656e73696f6e426f78202330313739"); assert(utxo); @@ -47,7 +45,6 @@ Deno.test("getUtxoByUnit", async () => { }); Deno.test("getUtxosByOutRef", async () => { - const koios = new Koios("Mainnet") try { const utxos: UTxO[] = await koios.getUtxosByOutRef([{txHash: 'c6ee20549eab1e565a4bed119bb8c7fc2d11cc5ea5e1e25433a34f0175c0bef6', outputIndex: 0}]); assert(utxos); @@ -57,10 +54,7 @@ Deno.test("getUtxosByOutRef", async () => { }); Deno.test("getDelegation", async () => { - const lucid = await Lucid.new( - new Koios("Mainnet"), - "Mainnet" - ); + const lucid = await Lucid.new(koios, "Mainnet"); assert(lucid) try { const delegation: Delegation = await lucid.delegationAt('stake1uyrx65wjqjgeeksd8hptmcgl5jfyrqkfq0xe8xlp367kphsckq250'); @@ -71,7 +65,6 @@ Deno.test("getDelegation", async () => { }); Deno.test("getDatum", async () => { - const koios = new Koios("Mainnet") try { const datum: Datum = await koios.getDatum('818ee3db3bbbd04f9f2ce21778cac3ac605802a4fcb00c8b3a58ee2dafc17d46'); assert(datum); @@ -81,7 +74,6 @@ Deno.test("getDatum", async () => { }); Deno.test("awaitTx", async () => { - const koios = new Koios("Mainnet") try { const isConfirmed: boolean = await koios.awaitTx('f144a8264acf4bdfe2e1241170969c930d64ab6b0996a4a45237b623f1dd670e'); assert(isConfirmed); @@ -91,7 +83,6 @@ Deno.test("awaitTx", async () => { }); Deno.test("submitTxBadRequest", async () => { - const koios = new Koios("Mainnet") try { const txId: string = await koios.submitTx('80'); assert(txId); From f0ecbd4bf5b925438b71e4a4541abfb253a6f81f Mon Sep 17 00:00:00 2001 From: Dudi Edri Date: Sun, 3 Sep 2023 15:59:27 +0300 Subject: [PATCH 4/5] Add Koios Provider to Lucid --- docs/docs/getting-started/choose-provider.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/docs/getting-started/choose-provider.md b/docs/docs/getting-started/choose-provider.md index 4d12f6e1..f9a7159c 100644 --- a/docs/docs/getting-started/choose-provider.md +++ b/docs/docs/getting-started/choose-provider.md @@ -60,6 +60,19 @@ const lucid = await Lucid.new( ); ``` +### Koios + +```js +import { Lucid, Koios } from "https://deno.land/x/lucid/mod.ts"; + +const lucid = await Lucid.new( + new Koios( + "https://prepod.koios.rest/api/v0", // For MAINNET: "https://api.koios.rest/api/v0". + ), + "Preprod", // For MAINNET: "Mainnet". +); +``` + ### Custom Lucid may add more providers in the future, but you also have the option to From 8f4392677ddf4c746e05ddd6ac37998d9842ac68 Mon Sep 17 00:00:00 2001 From: Dudi Edri Date: Tue, 5 Sep 2023 14:56:23 +0300 Subject: [PATCH 5/5] Add Koios Provider to Lucid | Fix Lint Issues --- src/provider/koios.ts | 234 +++++++++++++++++++++++------------------- 1 file changed, 126 insertions(+), 108 deletions(-) diff --git a/src/provider/koios.ts b/src/provider/koios.ts index c2d0ea57..3b121814 100644 --- a/src/provider/koios.ts +++ b/src/provider/koios.ts @@ -47,9 +47,10 @@ export class Koios implements Provider { async getUtxos(addressOrCredential: Address | Credential): Promise { if (typeof addressOrCredential === "string") { - const body: any = {} - body['_addresses'] = [addressOrCredential] - const result = await fetch(`${this.baseUrl}/address_info`, { + const body = { + '_addresses': [addressOrCredential] + } + const result: KoiosAddressInfoResponse = await fetch(`${this.baseUrl}/address_info`, { headers: { Accept: 'application/json', 'Content-Type': 'application/json' @@ -67,19 +68,19 @@ export class Koios implements Provider { } } - private async koiosUtxosToUtxos(result: any, address?: string): Promise { + private async koiosUtxosToUtxos(result: Array, address?: string): Promise { return (await Promise.all( - result.map(async (r: any) => ({ + result.map((r: KoiosUTxO) => ({ txHash: r.tx_hash, outputIndex: r.tx_index, assets: (() => { const a: Assets = {}; - r.asset_list.forEach((am: any) => { + r.asset_list.forEach((am: KoiosAsset) => { a[am.policy_id + am.asset_name] = BigInt(am.quantity); }); return a; })(), - address: address ? address : r.payment_addr.bech32, + address: address, datumHash: !r.inline_datum ? r.datum_hash : undefined, datum: r.inline_datum, scriptRef: { @@ -107,134 +108,114 @@ export class Koios implements Provider { } async getUtxoByUnit(unit: Unit): Promise { - let assetAddresses - try { - let {policyId, assetName} = fromUnit(unit) - assetName = String(assetName) - assetAddresses = await fetch(`${this.baseUrl}/asset_addresses?_asset_policy=${policyId}&_asset_name=${assetName}`) + let {policyId, assetName} = fromUnit(unit) + assetName = String(assetName) + const assetAddresses = await fetch(`${this.baseUrl}/asset_addresses?_asset_policy=${policyId}&_asset_name=${assetName}`) .then((res: Response) => res.json()); - } catch (e) { - throw new Error("Could not fetch UTxO from Koios. Try again."); - } if (Array.isArray(assetAddresses) && assetAddresses.length > 0) { if (assetAddresses.length > 1) { throw new Error("Unit needs to be an NFT or only held by one address."); } - const address = assetAddresses[0].payment_address - try { - const utxos: UTxO[] = await this.getUtxos(address) - const result = utxos.find((utxo): utxo is UTxO => { - const keys = Object.keys(utxo.assets) - return keys.length > 0 && keys.includes(unit) - }) - if (result) { - return result - } - } catch (e) { - throw new Error("Could not fetch UTxO from Koios. Try again."); + const utxos: UTxO[] = await this.getUtxos(assetAddresses[0].payment_address) + const result = utxos.find((utxo): utxo is UTxO => { + const keys = Object.keys(utxo.assets) + return keys.length > 0 && keys.includes(unit) + }) + if (result) { + return result } } throw new Error("Unit not found."); } async getUtxosByOutRef(outRefs: OutRef[]): Promise { - try { - const utxos = [] - const body: any = {} - body['_tx_hashes'] = [...new Set(outRefs.map((outRef) => outRef.txHash))]; - const result = await fetch(`${this.baseUrl}/tx_utxos`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - method: "POST", - body: JSON.stringify(body) - }).then((res: Response) => res.json()); - if (Array.isArray(result) && result.length > 0) { - for (const utxo of result) { - if (utxo.outputs && utxo.outputs.length > 0) { - utxos.push(await this.koiosUtxosToUtxos(utxo.outputs)) - } + const utxos = [] + const body = { + '_tx_hashes': [...new Set(outRefs.map((outRef) => outRef.txHash))] + } + const result = await fetch(`${this.baseUrl}/tx_utxos`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); + if (Array.isArray(result) && result.length > 0) { + for (const utxo of result) { + if (utxo.outputs && utxo.outputs.length > 0) { + utxos.push(await this.koiosUtxosToUtxos(utxo.outputs)) } - return utxos.reduce((acc, utxos) => acc.concat(utxos), []).filter((utxo) => - outRefs.some((outRef) => - utxo.txHash === outRef.txHash && utxo.outputIndex === outRef.outputIndex - ) - ); - } else { - return [] } - } catch (e) { - throw new Error("Could not fetch UTxOs from Koios. Try again."); + return utxos.reduce((acc, utxos) => acc.concat(utxos), []).filter((utxo) => + outRefs.some((outRef) => + utxo.txHash === outRef.txHash && utxo.outputIndex === outRef.outputIndex + ) + ); + } else { + return [] } } async getDelegation(rewardAddress: RewardAddress): Promise { - try { - const body: any = {} - body['_stake_addresses'] = [rewardAddress]; - const result = await fetch(`${this.baseUrl}/account_info`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - method: "POST", - body: JSON.stringify(body) - }).then((res: Response) => res.json()); - if (Array.isArray(result) && result.length > 0) { - return { - poolId: result[0].delegated_pool || null, - rewards: BigInt(result[0].rewards_available), - } + const body = { + '_stake_addresses': [rewardAddress] + } + const result = await fetch(`${this.baseUrl}/account_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); + if (Array.isArray(result) && result.length > 0) { + return { + poolId: result[0].delegated_pool || null, + rewards: BigInt(result[0].rewards_available), } - } catch (e) { - throw new Error("Could not fetch Account Information from Koios. Try again."); + } else { + throw new Error("No Delegation Found by Reward Address"); } - throw new Error("No Delegation Found by Reward Address"); } async getDatum(datumHash: DatumHash): Promise { - try { - const body: any = {} - body['_datum_hashes'] = [datumHash]; - const result = await fetch(`${this.baseUrl}/datum_info`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - method: "POST", - body: JSON.stringify(body) - }).then((res: Response) => res.json()); - if (Array.isArray(result) && result.length > 0) { - return result[0].bytes - } - } catch (e) { - throw new Error("Could not fetch Datum Information from Koios. Try again."); + const body = { + '_datum_hashes': [datumHash] + } + const datum = await fetch(`${this.baseUrl}/datum_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); + if (Array.isArray(datum) && datum.length > 0) { + return datum[0].bytes + } else { + throw new Error("No Datum Found by Datum Hash"); } - throw new Error("No Datum Found by Datum Hash"); } awaitTx(txHash: TxHash, checkInterval = 3000): Promise { return new Promise((res) => { const confirmation = setInterval(async () => { - try { - const body: any = {} - body['_tx_hashes'] = [txHash]; - const result = await fetch(`${this.baseUrl}/tx_info`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - method: "POST", - body: JSON.stringify(body) - }).then((res: Response) => res.json()); - if (Array.isArray(result) && result.length > 0) { - clearInterval(confirmation); - await new Promise((res) => setTimeout(() => res(1), 1000)); - return res(true) - } - } catch (e) { - throw new Error("Could not fetch Datum Information from Koios. Try again."); + const body = { + '_tx_hashes': [txHash] + } + const result = await fetch(`${this.baseUrl}/tx_info`, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(body) + }).then((res: Response) => res.json()); + if (Array.isArray(result) && result.length > 0) { + clearInterval(confirmation); + await new Promise((res) => setTimeout(() => res(1), 1000)); + return res(true) } }, checkInterval); }); @@ -255,4 +236,41 @@ export class Koios implements Provider { } return result } -} \ No newline at end of file +} + +type KoiosAddressInfoResponse = Array<{ + address: Address; + balance: string; + stake_address?: string + script_address: string + utxo_set: Array +}>; + +type KoiosUTxO = { + tx_hash: string; + tx_index: number; + block_height?: number; + block_time: number; + value: string; + datum_hash?: string; + inline_datum?: { + bytes: string; + value: object; + } + reference_script?: { + hash: string; + size: number; + type: string; + bytes: string; + value?: object; + } + asset_list: Array +} + +type KoiosAsset = { + policy_id: string; + asset_name?: string; + fingerprint: string; + decimals: number; + quantity: string; +}; \ No newline at end of file