diff --git a/packages/cashscript/package.json b/packages/cashscript/package.json index b725ea8b..84fd8264 100644 --- a/packages/cashscript/package.json +++ b/packages/cashscript/package.json @@ -46,6 +46,7 @@ "@bitauth/libauth": "^3.1.0-next.2", "@cashscript/utils": "^0.11.0-next.0", "@mr-zwets/bchn-api-wrapper": "^1.0.1", + "chaingraph-ts": "^0.2.3", "delay": "^6.0.0", "electrum-cash": "^2.0.10", "fast-deep-equal": "^3.1.3", diff --git a/packages/cashscript/src/index.ts b/packages/cashscript/src/index.ts index 53c364bb..c3e32af9 100644 --- a/packages/cashscript/src/index.ts +++ b/packages/cashscript/src/index.ts @@ -19,5 +19,6 @@ export { ElectrumNetworkProvider, FullStackNetworkProvider, MockNetworkProvider, + ChaingraphNetworkProvider, } from './network/index.js'; export { randomUtxo, randomToken, randomNFT } from './utils.js'; diff --git a/packages/cashscript/src/network/ChaingraphNetworkProvider.ts b/packages/cashscript/src/network/ChaingraphNetworkProvider.ts new file mode 100644 index 00000000..d4532bac --- /dev/null +++ b/packages/cashscript/src/network/ChaingraphNetworkProvider.ts @@ -0,0 +1,60 @@ +import { ChaingraphClient } from 'chaingraph-ts'; +import { Utxo, Network } from '../interfaces.js'; +import NetworkProvider from './NetworkProvider.js'; + +export default class ChaingraphNetworkProvider implements NetworkProvider { + private client: ChaingraphClient; + + constructor( + public network: Network, + chaingraphOrUrl: ChaingraphClient | string + ) { + if (chaingraphOrUrl instanceof ChaingraphClient) { + this.client = chaingraphOrUrl; + } else if (typeof chaingraphOrUrl === 'string') { + this.client = new ChaingraphClient(chaingraphOrUrl); + } else { + throw new Error( + 'Invalid parameter. Must be an instance of ChaingraphClient or a chaingraph url' + ); + } + } + + async getUtxos(address: string): Promise { + const result = await this.client.getUtxosForAddress(address); + + const utxos: Utxo[] = result.map((utxo) => ({ + txid: utxo.transaction_hash, + vout: Number(utxo.output_index), + satoshis: BigInt(utxo.value_satoshis), + token: utxo.token_category + ? { + category: utxo.token_category, + amount: BigInt(utxo.fungible_token_amount!), + nft: utxo.nonfungible_token_commitment + ? { + capability: utxo.nonfungible_token_capability!, + commitment: utxo.nonfungible_token_commitment, + } + : undefined, + } + : undefined, + })); + + return utxos; + } + + async getBlockHeight(): Promise { + return this.client.getBlockHeight(); + } + + async getRawTransaction(txid: string): Promise { + const response = await this.client.getRawTransaction(txid); + return response!; + } + + async sendRawTransaction(txHex: string): Promise { + const response = await this.client.sendRawTransaction(txHex); + return response.send_transaction.transaction_hash; + } +} diff --git a/packages/cashscript/src/network/index.ts b/packages/cashscript/src/network/index.ts index 8d31ddde..7094b969 100644 --- a/packages/cashscript/src/network/index.ts +++ b/packages/cashscript/src/network/index.ts @@ -3,3 +3,4 @@ export { default as BitcoinRpcNetworkProvider } from './BitcoinRpcNetworkProvide export { default as ElectrumNetworkProvider } from './ElectrumNetworkProvider.js'; export { default as FullStackNetworkProvider } from './FullStackNetworkProvider.js'; export { default as MockNetworkProvider } from './MockNetworkProvider.js'; +export { default as ChaingraphNetworkProvider } from './ChaingraphNetworkProvider.js'; diff --git a/packages/cashscript/test/e2e/network/Chaingraph.test.ts b/packages/cashscript/test/e2e/network/Chaingraph.test.ts new file mode 100644 index 00000000..47566f8e --- /dev/null +++ b/packages/cashscript/test/e2e/network/Chaingraph.test.ts @@ -0,0 +1,75 @@ +import { Contract, SignatureTemplate, ChaingraphNetworkProvider } from '../../../src/index.js'; +import { + alicePriv, + bobPkh, + bobPriv, + bobPub, +} from '../../fixture/vars.js'; +import { getTxOutputs } from '../../test-util.js'; +import { FailedRequireError } from '../../../src/Errors.js'; +import artifact from '../../fixture/p2pkh.json' with { type: 'json' }; + +const describeOrSkip = process.env.TESTS_USE_MOCKNET ? describe.skip : describe; + +describeOrSkip('test ChaingraphNetworkProvider', () => { + const provider = new ChaingraphNetworkProvider('mainnet', "https://gql.chaingraph.pat.mn/v1/graphql"); + + describe('get utxos using ChaingraphNetworkProvider', () => { + it('should get the utxos for a p2sh20 contract', async () => { + // Note: We instantiate the contract with bobPkh to avoid mempool conflicts with other tests + const p2pkhInstance = new Contract(artifact, [bobPkh], { provider, addressType: 'p2sh20' }); + console.log(p2pkhInstance.address); + + const utxos = await p2pkhInstance.getUtxos(); + expect(Array.isArray(utxos)).toBe(true); + }); + it('should get the utxos for a p2sh32 contract', async () => { + // Note: We instantiate the contract with bobPkh to avoid mempool conflicts with other tests + const p2pkhInstance = new Contract(artifact, [bobPkh], { provider, addressType: 'p2sh32' }); + console.log(p2pkhInstance.address); + + const utxos = await p2pkhInstance.getUtxos(); + expect(Array.isArray(utxos)).toBe(true); + }); + }); + + describe('send using ChaingraphNetworkProvider', () => { + // Note: We instantiate the contract with bobPkh to avoid mempool conflicts with other tests + // Using p2sh20 address because it is funded on mainnet + const p2pkhInstance = new Contract(artifact, [bobPkh], { provider, addressType: 'p2sh20' }); + console.log(p2pkhInstance.address); + + it('should fail when using incorrect function arguments', async () => { + // given + const to = p2pkhInstance.address; + const amount = 10000n; + + // when + const txPromise = p2pkhInstance.functions + .spend(bobPub, new SignatureTemplate(alicePriv)) + .to(to, amount) + .send(); + + // then + await expect(txPromise).rejects.toThrow(FailedRequireError); + await expect(txPromise).rejects.toThrow('P2PKH.cash:5 Require statement failed at input 0 in contract P2PKH.cash at line 5.'); + }); + + it('should succeed when using correct function arguments', async () => { + // given + const to = p2pkhInstance.address; + const amount = 10000n; + + // when + const tx = await p2pkhInstance.functions + .spend(bobPub, new SignatureTemplate(bobPriv)) + .to(to, amount) + .send(); + + // then + const txOutputs = getTxOutputs(tx, 'mainnet'); + expect(txOutputs).toEqual(expect.arrayContaining([{ to, amount }])); + expect(tx.txid).toBeDefined(); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 15fa8ffb..f42a288c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,19 @@ # yarn lockfile v1 +"@0no-co/graphql.web@^1.0.5": + version "1.0.13" + resolved "https://registry.yarnpkg.com/@0no-co/graphql.web/-/graphql.web-1.0.13.tgz#978f4d3a869240f2d487fa1c1009028b34bc33b5" + integrity sha512-jqYxOevheVTU1S36ZdzAkJIdvRp2m3OYIG5SEoKDw5NI8eVwkoI0D/Q3DYNGmXCxkA6CQuoa7zvMiDPTLqUNuw== + +"@0no-co/graphqlsp@^1.12.13": + version "1.12.16" + resolved "https://registry.yarnpkg.com/@0no-co/graphqlsp/-/graphqlsp-1.12.16.tgz#58fe7bad53b3ad9fdf2d5f41ddeb9b418d289a03" + integrity sha512-B5pyYVH93Etv7xjT6IfB7QtMBdaaC07yjbhN6v8H7KgFStMkPvi+oWYBTibMFRMY89qwc9H8YixXg8SXDVgYWw== + dependencies: + "@gql.tada/internal" "^1.0.0" + graphql "^15.5.0 || ^16.0.0 || ^17.0.0" + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -1287,6 +1300,22 @@ unique-filename "^1.1.1" which "^1.3.1" +"@gql.tada/cli-utils@1.6.3": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@gql.tada/cli-utils/-/cli-utils-1.6.3.tgz#b893cec74908da4df0602691e2e0b1497fda8cda" + integrity sha512-jFFSY8OxYeBxdKi58UzeMXG1tdm4FVjXa8WHIi66Gzu9JWtCE6mqom3a8xkmSw+mVaybFW5EN2WXf1WztJVNyQ== + dependencies: + "@0no-co/graphqlsp" "^1.12.13" + "@gql.tada/internal" "1.0.8" + graphql "^15.5.0 || ^16.0.0 || ^17.0.0" + +"@gql.tada/internal@1.0.8", "@gql.tada/internal@^1.0.0": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@gql.tada/internal/-/internal-1.0.8.tgz#3ba9fa6534809788bbbe103492f70b8e9d754027" + integrity sha512-XYdxJhtHC5WtZfdDqtKjcQ4d7R1s0d1rnlSs3OcBEUbYiPoJJfZU7tWsVXuv047Z6msvmr4ompJ7eLSK5Km57g== + dependencies: + "@0no-co/graphql.web" "^1.0.5" + "@humanwhocodes/config-array@^0.11.13", "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -2883,6 +2912,14 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@urql/core@^5.0.8": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@urql/core/-/core-5.1.0.tgz#7f4b81f1aba1ca34ae6354763abeb87ff9af84ff" + integrity sha512-yC3sw8yqjbX45GbXxfiBY8GLYCiyW/hLBbQF9l3TJrv4ro00Y0ChkKaD9I2KntRxAVm9IYBqh0awX8fwWAe/Yw== + dependencies: + "@0no-co/graphql.web" "^1.0.5" + wonka "^6.3.2" + "@zkochan/cmd-shim@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e" @@ -3821,6 +3858,17 @@ cashaddrjs-slp@^0.2.11: dependencies: big-integer "^1.6.34" +chaingraph-ts@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/chaingraph-ts/-/chaingraph-ts-0.2.3.tgz#0d6a029fe8a0d99934431f4eda18771297825e7e" + integrity sha512-21lJoTaXMrp0pBZX1Cl51TdIUY5OIUZ7dXYRO9jY+Tpgjkc882cQpWMZw+LPqE3sHbgXkMfQEepEwbI7Hay3CA== + dependencies: + "@bitauth/libauth" "^3.0.0" + "@urql/core" "^5.0.8" + gql.tada "^1.8.10" + graphql-ws "^5.16.0" + ws "^8.18.0" + chalk-template@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-1.1.0.tgz#ffc55db6dd745e9394b85327c8ac8466edb7a7b1" @@ -6059,6 +6107,16 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +gql.tada@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/gql.tada/-/gql.tada-1.8.10.tgz#096a1b30d3c6fc74212fe07d507f01a4095f7f67" + integrity sha512-FrvSxgz838FYVPgZHGOSgbpOjhR+yq44rCzww3oOPJYi0OvBJjAgCiP6LEokZIYND2fUTXzQAyLgcvgw1yNP5A== + dependencies: + "@0no-co/graphql.web" "^1.0.5" + "@0no-co/graphqlsp" "^1.12.13" + "@gql.tada/cli-utils" "1.6.3" + "@gql.tada/internal" "1.0.8" + graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" @@ -6079,6 +6137,16 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-ws@^5.16.0: + version "5.16.2" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.2.tgz#7b0306c1bdb0e97a05e800ccd523f46fb212e37c" + integrity sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ== + +"graphql@^15.5.0 || ^16.0.0 || ^17.0.0": + version "16.10.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" + integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -11094,6 +11162,11 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" +wonka@^6.3.2: + version "6.3.4" + resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.3.4.tgz#76eb9316e3d67d7febf4945202b5bdb2db534594" + integrity sha512-CjpbqNtBGNAeyNS/9W6q3kSkKE52+FjIj7AkFlLr11s/VWGUu6a2CdYSdGxocIhIVjaW/zchesBQUKPVU69Cqg== + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" @@ -11191,6 +11264,11 @@ ws@^7.5.2: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + xdg-basedir@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9"