diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Asset By Code And Scale.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Asset By Code And Scale.bru new file mode 100644 index 0000000000..4f2e88f7d9 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Asset By Code And Scale.bru @@ -0,0 +1,50 @@ +meta { + name: Get Asset By Code And Scale + type: graphql + seq: 52 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetAssetByCodeAndScale($code: String!, $scale: UInt8!) { + assetByCodeAndScale(code: $code, scale: $scale) { + code + createdAt + id + scale + withdrawalThreshold + liquidityThreshold + sendingFee { + id + type + basisPoints + fixed + } + receivingFee { + id + type + basisPoints + fixed + } + } + } + +} + +body:graphql:vars { + { + "code": "USD", + "scale": 2 + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Peer By Address and Asset Id.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Peer By Address and Asset Id.bru new file mode 100644 index 0000000000..a00d70c013 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Peer By Address and Asset Id.bru @@ -0,0 +1,42 @@ +meta { + name: Get Peer By Address and Asset Id + type: graphql + seq: 51 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetPeerByAddressAndAsset($address: String!, $assetId: String!) { + peerByAddressAndAsset(staticIlpAddress: $address, assetId: $assetId) { + id + name + http { + outgoing { + authToken + endpoint + } + } + liquidity + liquidityThreshold + } + } + +} + +body:graphql:vars { + { + "address": "test.happy-life-bank", + "assetId": "b7acf591-f77f-404b-bb31-d9ac96aba1f9" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/Rafiki Admin APIs/Get Wallet Address By Url.bru b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Wallet Address By Url.bru new file mode 100644 index 0000000000..5a9ceb4880 --- /dev/null +++ b/bruno/collections/Rafiki/Rafiki Admin APIs/Get Wallet Address By Url.bru @@ -0,0 +1,88 @@ +meta { + name: Get Wallet Address By Url + type: graphql + seq: 53 +} + +post { + url: {{RafikiGraphqlHost}}/graphql + body: graphql + auth: none +} + +body:graphql { + query GetWalletAddress($url: String!) { + walletAddressByUrl(url: $url) { + id + asset { + id + code + scale + withdrawalThreshold + createdAt + } + createdAt + incomingPayments { + edges { + node { + id + state + incomingAmount { + value + } + receivedAmount { + value + } + } + cursor + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + } + walletAddressKeys { + edges { + node { + id + jwk { + kid + x + alg + kty + crv + } + revoked + createdAt + } + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + } + status + additionalProperties { + key + value + visibleInOpenPayments + } + } + } +} + +body:graphql:vars { + { + "url": "https://cloud-nine-wallet-backend/accounts/broke" + } +} + +script:pre-request { + const scripts = require('./scripts'); + + scripts.addApiSignatureHeader(); +} diff --git a/bruno/collections/Rafiki/environments/Local Playground.bru b/bruno/collections/Rafiki/environments/Local Playground.bru index 59244355df..6cb1033ff6 100644 --- a/bruno/collections/Rafiki/environments/Local Playground.bru +++ b/bruno/collections/Rafiki/environments/Local Playground.bru @@ -28,4 +28,6 @@ vars { authApiSignatureVersion: 1 authApiSignatureSecret: rPoZpe9tVyBNCigm05QDco7WLcYa0xMao7lO5KG1XG4= assetIdTigerBeetle: 1 + assetCode: USD + assetScale: 2 } diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index 131de7eb44..9437d6ec24 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -1124,6 +1124,8 @@ export type Query = { accountingTransfers: AccountingTransferConnection; /** Fetch an asset by its ID. */ asset?: Maybe; + /** Get an asset based on its currency code and scale if it exists. */ + assetByCodeAndScale?: Maybe; /** Fetch a paginated list of assets. */ assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ @@ -1136,6 +1138,8 @@ export type Query = { payments: PaymentConnection; /** Fetch a peer by its ID. */ peer?: Maybe; + /** Get a peer based on its ILP address and asset ID if it exists. */ + peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; /** Fetch an Open Payments quote by its ID. */ @@ -1144,6 +1148,8 @@ export type Query = { receiver?: Maybe; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; + /** Get a wallet address by its url if it exists */ + walletAddressByUrl?: Maybe; /** Fetch a paginated list of wallet addresses. */ walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ @@ -1162,6 +1168,12 @@ export type QueryAssetArgs = { }; +export type QueryAssetByCodeAndScaleArgs = { + code: Scalars['String']['input']; + scale: Scalars['UInt8']['input']; +}; + + export type QueryAssetsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1206,6 +1218,12 @@ export type QueryPeerArgs = { }; +export type QueryPeerByAddressAndAssetArgs = { + assetId: Scalars['String']['input']; + staticIlpAddress: Scalars['String']['input']; +}; + + export type QueryPeersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1230,6 +1248,11 @@ export type QueryWalletAddressArgs = { }; +export type QueryWalletAddressByUrlArgs = { + url: Scalars['String']['input']; +}; + + export type QueryWalletAddressesArgs = { after?: InputMaybe; before?: InputMaybe; @@ -2275,16 +2298,19 @@ export type PeersConnectionResolvers = { accountingTransfers?: Resolver>; asset?: Resolver, ParentType, ContextType, RequireFields>; + assetByCodeAndScale?: Resolver, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; peer?: Resolver, ParentType, ContextType, RequireFields>; + peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; + walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; }; diff --git a/packages/backend/src/asset/service.ts b/packages/backend/src/asset/service.ts index 139006e926..8967fc5d32 100644 --- a/packages/backend/src/asset/service.ts +++ b/packages/backend/src/asset/service.ts @@ -33,6 +33,7 @@ export interface AssetService { update(options: UpdateOptions): Promise delete(options: DeleteOptions): Promise get(id: string): Promise + getByCodeAndScale(code: string, scale: number): Promise getPage(pagination?: Pagination, sortOrder?: SortOrder): Promise getAll(): Promise } @@ -59,6 +60,8 @@ export async function createAssetService({ update: (options) => updateAsset(deps, options), delete: (options) => deleteAsset(deps, options), get: (id) => getAsset(deps, id), + getByCodeAndScale: (code, scale) => + getAssetByCodeAndScale(deps, code, scale), getPage: (pagination?, sortOrder?) => getAssetsPage(deps, pagination, sortOrder), getAll: () => getAll(deps) @@ -171,6 +174,16 @@ async function getAsset( return await Asset.query(deps.knex).whereNull('deletedAt').findById(id) } +async function getAssetByCodeAndScale( + deps: ServiceDependencies, + code: string, + scale: number +): Promise { + return await Asset.query(deps.knex) + .where({ code: code, scale: scale }) + .first() +} + async function getAssetsPage( deps: ServiceDependencies, pagination?: Pagination, diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 3f5f20485e..f0a9d3f643 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -6215,6 +6215,51 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "assetByCodeAndScale", + "description": "Get an asset based on its currency code and scale if it exists.", + "args": [ + { + "name": "code", + "description": "ISO 4217 currency code.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scale", + "description": "Difference in order of magnitude between the standard unit of an asset and its corresponding fractional unit.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "UInt8", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Asset", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "assets", "description": "Fetch a paginated list of assets.", @@ -6557,6 +6602,51 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "peerByAddressAndAsset", + "description": "Get a peer based on its ILP address and asset ID if it exists.", + "args": [ + { + "name": "assetId", + "description": "Asset ID of peering relationship.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "staticIlpAddress", + "description": "ILP address of the peer.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Peer", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "peers", "description": "Fetch a paginated list of peers.", @@ -6721,6 +6811,35 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "walletAddressByUrl", + "description": "Get a wallet address by its url if it exists", + "args": [ + { + "name": "url", + "description": "Wallet Address URL.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "OBJECT", + "name": "WalletAddress", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "walletAddresses", "description": "Fetch a paginated list of wallet addresses.", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index 131de7eb44..9437d6ec24 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -1124,6 +1124,8 @@ export type Query = { accountingTransfers: AccountingTransferConnection; /** Fetch an asset by its ID. */ asset?: Maybe; + /** Get an asset based on its currency code and scale if it exists. */ + assetByCodeAndScale?: Maybe; /** Fetch a paginated list of assets. */ assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ @@ -1136,6 +1138,8 @@ export type Query = { payments: PaymentConnection; /** Fetch a peer by its ID. */ peer?: Maybe; + /** Get a peer based on its ILP address and asset ID if it exists. */ + peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; /** Fetch an Open Payments quote by its ID. */ @@ -1144,6 +1148,8 @@ export type Query = { receiver?: Maybe; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; + /** Get a wallet address by its url if it exists */ + walletAddressByUrl?: Maybe; /** Fetch a paginated list of wallet addresses. */ walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ @@ -1162,6 +1168,12 @@ export type QueryAssetArgs = { }; +export type QueryAssetByCodeAndScaleArgs = { + code: Scalars['String']['input']; + scale: Scalars['UInt8']['input']; +}; + + export type QueryAssetsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1206,6 +1218,12 @@ export type QueryPeerArgs = { }; +export type QueryPeerByAddressAndAssetArgs = { + assetId: Scalars['String']['input']; + staticIlpAddress: Scalars['String']['input']; +}; + + export type QueryPeersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1230,6 +1248,11 @@ export type QueryWalletAddressArgs = { }; +export type QueryWalletAddressByUrlArgs = { + url: Scalars['String']['input']; +}; + + export type QueryWalletAddressesArgs = { after?: InputMaybe; before?: InputMaybe; @@ -2275,16 +2298,19 @@ export type PeersConnectionResolvers = { accountingTransfers?: Resolver>; asset?: Resolver, ParentType, ContextType, RequireFields>; + assetByCodeAndScale?: Resolver, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; peer?: Resolver, ParentType, ContextType, RequireFields>; + peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; + walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; }; diff --git a/packages/backend/src/graphql/resolvers/asset.test.ts b/packages/backend/src/graphql/resolvers/asset.test.ts index d174e32a2b..7968de3f98 100644 --- a/packages/backend/src/graphql/resolvers/asset.test.ts +++ b/packages/backend/src/graphql/resolvers/asset.test.ts @@ -280,6 +280,70 @@ describe('Asset Resolvers', (): void => { }) }) + test('Can get an asset by code and scale', async (): Promise => { + const asset = await assetService.create({ + ...randomAsset(), + withdrawalThreshold: BigInt(10), + liquidityThreshold: BigInt(100) + }) + assert.ok(!isAssetError(asset)) + assert.ok(asset.withdrawalThreshold) + const args = { code: asset.code, scale: asset.scale } + const query = async () => + await appContainer.apolloClient + .query({ + query: gql` + query GetAssetByCodeAndScale($code: String!, $scale: UInt8!) { + assetByCodeAndScale(code: $code, scale: $scale) { + id + code + scale + liquidity + withdrawalThreshold + liquidityThreshold + createdAt + } + } + `, + variables: args + }) + .then((query): Asset => { + if (query.data) { + return query.data.assetByCodeAndScale + } else { + throw new Error('Data was empty') + } + }) + + await expect(query()).resolves.toEqual({ + __typename: 'Asset', + id: asset.id, + code: asset.code, + scale: asset.scale, + liquidity: '0', + withdrawalThreshold: asset.withdrawalThreshold.toString(), + liquidityThreshold: asset.liquidityThreshold?.toString(), + createdAt: new Date(+asset.createdAt).toISOString() + }) + + await accountingService.createDeposit({ + id: uuid(), + account: asset, + amount: BigInt(100) + }) + + await expect(query()).resolves.toEqual({ + __typename: 'Asset', + id: asset.id, + code: asset.code, + scale: asset.scale, + liquidity: '100', + withdrawalThreshold: asset.withdrawalThreshold.toString(), + liquidityThreshold: asset.liquidityThreshold?.toString(), + createdAt: new Date(+asset.createdAt).toISOString() + }) + }) + test.each([ undefined, { fixed: BigInt(100), basisPoints: 1000, type: FeeType.Sending }, diff --git a/packages/backend/src/graphql/resolvers/asset.ts b/packages/backend/src/graphql/resolvers/asset.ts index 52a18a2b30..50638d721c 100644 --- a/packages/backend/src/graphql/resolvers/asset.ts +++ b/packages/backend/src/graphql/resolvers/asset.ts @@ -56,6 +56,13 @@ export const getAsset: QueryResolvers['asset'] = async ( return assetToGraphql(asset) } +export const getAssetByCodeAndScale: QueryResolvers['assetByCodeAndScale'] = + async (parent, args, ctx): Promise => { + const assetService = await ctx.container.use('assetService') + const asset = await assetService.getByCodeAndScale(args.code, args.scale) + return asset ? assetToGraphql(asset) : null + } + export const createAsset: MutationResolvers['createAsset'] = async ( parent, diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index d71a771a97..2c191b30e8 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -4,7 +4,8 @@ import { getWalletAddresses, createWalletAddress, updateWalletAddress, - triggerWalletAddressEvents + triggerWalletAddressEvents, + getWalletAddressByUrl } from './wallet_address' import { getAsset, @@ -14,7 +15,8 @@ import { deleteAsset, getAssetReceivingFee, getAssetSendingFee, - getFees + getFees, + getAssetByCodeAndScale } from './asset' import { getWalletAddressIncomingPayments, @@ -33,7 +35,14 @@ import { createOutgoingPaymentFromIncomingPayment, cancelOutgoingPayment } from './outgoing_payment' -import { getPeer, getPeers, createPeer, updatePeer, deletePeer } from './peer' +import { + getPeer, + getPeers, + createPeer, + updatePeer, + deletePeer, + getPeerByAddressAndAsset +} from './peer' import { getAssetLiquidity, getPeerLiquidity, @@ -84,13 +93,16 @@ export const resolvers: Resolvers = { }, Query: { walletAddress: getWalletAddress, + walletAddressByUrl: getWalletAddressByUrl, walletAddresses: getWalletAddresses, asset: getAsset, assets: getAssets, + assetByCodeAndScale: getAssetByCodeAndScale, outgoingPayment: getOutgoingPayment, outgoingPayments: getOutgoingPayments, incomingPayment: getIncomingPayment, peer: getPeer, + peerByAddressAndAsset: getPeerByAddressAndAsset, peers: getPeers, quote: getQuote, webhookEvents: getWebhookEvents, diff --git a/packages/backend/src/graphql/resolvers/peer.test.ts b/packages/backend/src/graphql/resolvers/peer.test.ts index 8fc24012d2..4aec23a2e8 100644 --- a/packages/backend/src/graphql/resolvers/peer.test.ts +++ b/packages/backend/src/graphql/resolvers/peer.test.ts @@ -334,6 +334,105 @@ describe('Peer Resolvers', (): void => { }) }) + test('Can get a peer by address and asset id', async (): Promise => { + const peer = await createPeer(deps, randomPeer()) + const args = { + staticIlpAddress: peer.staticIlpAddress, + assetId: peer.assetId + } + + const query = async () => + await appContainer.apolloClient + .query({ + query: gql` + query getPeerByAddressAndAsset( + $staticIlpAddress: String! + $assetId: String! + ) { + peerByAddressAndAsset( + staticIlpAddress: $staticIlpAddress + assetId: $assetId + ) { + id + asset { + code + scale + } + maxPacketAmount + http { + outgoing { + authToken + endpoint + } + } + staticIlpAddress + liquidity + name + liquidityThreshold + } + } + `, + variables: args + }) + .then((query): GraphQLPeer => { + if (query.data) { + return query.data.peerByAddressAndAsset + } else { + throw new Error('Data was empty') + } + }) + + await expect(query()).resolves.toEqual({ + __typename: 'Peer', + id: peer.id, + asset: { + __typename: 'Asset', + code: peer.asset.code, + scale: peer.asset.scale + }, + http: { + __typename: 'Http', + outgoing: { + __typename: 'HttpOutgoing', + ...peer.http.outgoing + } + }, + staticIlpAddress: peer.staticIlpAddress, + maxPacketAmount: peer.maxPacketAmount?.toString(), + liquidity: '0', + name: peer.name, + liquidityThreshold: '100' + }) + + await accountingService.createDeposit({ + id: uuid(), + account: peer, + amount: BigInt(100) + }) + + await expect(query()).resolves.toEqual({ + __typename: 'Peer', + id: peer.id, + asset: { + __typename: 'Asset', + code: peer.asset.code, + scale: peer.asset.scale + }, + http: { + __typename: 'Http', + outgoing: { + __typename: 'HttpOutgoing', + ...peer.http.outgoing + } + }, + staticIlpAddress: peer.staticIlpAddress, + maxPacketAmount: peer.maxPacketAmount?.toString(), + liquidity: '100', + name: peer.name, + liquidityThreshold: '100' + }) + }) + test('Returns error for unknown peer', async (): Promise => { expect.assertions(2) try { diff --git a/packages/backend/src/graphql/resolvers/peer.ts b/packages/backend/src/graphql/resolvers/peer.ts index b2296b7a89..d14b73603d 100644 --- a/packages/backend/src/graphql/resolvers/peer.ts +++ b/packages/backend/src/graphql/resolvers/peer.ts @@ -58,6 +58,16 @@ export const getPeer: QueryResolvers['peer'] = async ( return peerToGraphql(peer) } +export const getPeerByAddressAndAsset: QueryResolvers['peerByAddressAndAsset'] = + async (parent, args, ctx): Promise => { + const peerService = await ctx.container.use('peerService') + const peer = await peerService.getByDestinationAddress( + args.staticIlpAddress, + args.assetId + ) + return peer ? peerToGraphql(peer) : null + } + export const createPeer: MutationResolvers['createPeer'] = async ( parent, diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index ff51dd0d1f..12e55044ca 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -718,6 +718,65 @@ describe('Wallet Address Resolvers', (): void => { } ) + test.each` + publicName + ${'Alice'} + ${undefined} + `( + 'Can get a wallet address by its url (publicName: $publicName)', + async ({ publicName }): Promise => { + const walletAddress = await createWalletAddress(deps, { + publicName, + createLiquidityAccount: true + }) + const args = { url: walletAddress.url } + const query = await appContainer.apolloClient + .query({ + query: gql` + query getWalletAddressByUrl($url: String!) { + walletAddressByUrl(url: $url) { + id + liquidity + asset { + code + scale + } + url + publicName + additionalProperties { + key + value + visibleInOpenPayments + } + } + } + `, + variables: args + }) + .then((query): WalletAddress => { + if (query.data) { + return query.data.walletAddressByUrl + } else { + throw new Error('Data was empty') + } + }) + + expect(query).toEqual({ + __typename: 'WalletAddress', + id: walletAddress.id, + liquidity: '0', + asset: { + __typename: 'Asset', + code: walletAddress.asset.code, + scale: walletAddress.asset.scale + }, + url: walletAddress.url, + publicName: publicName ?? null, + additionalProperties: [] + }) + } + ) + test('Returns error for unknown wallet address', async (): Promise => { expect.assertions(2) try { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.ts b/packages/backend/src/graphql/resolvers/wallet_address.ts index 1731699fc9..d1f7172dab 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.ts @@ -69,6 +69,17 @@ export const getWalletAddress: QueryResolvers['walletAddress'] = return walletAddressToGraphql(walletAddress) } +export const getWalletAddressByUrl: QueryResolvers['walletAddressByUrl'] = + async ( + parent, + args, + ctx + ): Promise => { + const walletAddressService = await ctx.container.use('walletAddressService') + const walletAddress = await walletAddressService.getByUrl(args.url) + return walletAddress ? walletAddressToGraphql(walletAddress) : null + } + export const createWalletAddress: MutationResolvers['createWalletAddress'] = async ( parent, diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index b6a57d5523..a41dcd85ac 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -2,6 +2,14 @@ type Query { "Fetch an asset by its ID." asset("Unique identifier of the asset." id: String!): Asset + "Get an asset based on its currency code and scale if it exists." + assetByCodeAndScale( + "ISO 4217 currency code." + code: String! + "Difference in order of magnitude between the standard unit of an asset and its corresponding fractional unit." + scale: UInt8! + ): Asset + "Fetch a paginated list of assets." assets( "Forward pagination: Cursor (asset ID) to start retrieving assets after this point." @@ -19,6 +27,14 @@ type Query { "Fetch a peer by its ID." peer("Unique identifier of the peer." id: String!): Peer + "Get a peer based on its ILP address and asset ID if it exists." + peerByAddressAndAsset( + "ILP address of the peer." + staticIlpAddress: String! + "Asset ID of peering relationship." + assetId: String! + ): Peer + "Fetch a paginated list of peers." peers( "Forward pagination: Cursor (peer ID) to start retrieving peers after this point." @@ -39,6 +55,9 @@ type Query { id: String! ): WalletAddress + "Get a wallet address by its url if it exists" + walletAddressByUrl("Wallet Address URL." url: String!): WalletAddress + "Fetch a paginated list of wallet addresses." walletAddresses( "Forward pagination: Cursor (wallet address ID) to start retrieving wallet addresses after this point." diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index 2d35119c43..4bc2279d6f 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -1124,6 +1124,8 @@ export type Query = { accountingTransfers: AccountingTransferConnection; /** Fetch an asset by its ID. */ asset?: Maybe; + /** Get an asset based on its currency code and scale if it exists. */ + assetByCodeAndScale?: Maybe; /** Fetch a paginated list of assets. */ assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ @@ -1136,6 +1138,8 @@ export type Query = { payments: PaymentConnection; /** Fetch a peer by its ID. */ peer?: Maybe; + /** Get a peer based on its ILP address and asset ID if it exists. */ + peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; /** Fetch an Open Payments quote by its ID. */ @@ -1144,6 +1148,8 @@ export type Query = { receiver?: Maybe; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; + /** Get a wallet address by its url if it exists */ + walletAddressByUrl?: Maybe; /** Fetch a paginated list of wallet addresses. */ walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ @@ -1162,6 +1168,12 @@ export type QueryAssetArgs = { }; +export type QueryAssetByCodeAndScaleArgs = { + code: Scalars['String']['input']; + scale: Scalars['UInt8']['input']; +}; + + export type QueryAssetsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1206,6 +1218,12 @@ export type QueryPeerArgs = { }; +export type QueryPeerByAddressAndAssetArgs = { + assetId: Scalars['String']['input']; + staticIlpAddress: Scalars['String']['input']; +}; + + export type QueryPeersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1230,6 +1248,11 @@ export type QueryWalletAddressArgs = { }; +export type QueryWalletAddressByUrlArgs = { + url: Scalars['String']['input']; +}; + + export type QueryWalletAddressesArgs = { after?: InputMaybe; before?: InputMaybe; @@ -2275,16 +2298,19 @@ export type PeersConnectionResolvers = { accountingTransfers?: Resolver>; asset?: Resolver, ParentType, ContextType, RequireFields>; + assetByCodeAndScale?: Resolver, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; peer?: Resolver, ParentType, ContextType, RequireFields>; + peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; + walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index 131de7eb44..9437d6ec24 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -1124,6 +1124,8 @@ export type Query = { accountingTransfers: AccountingTransferConnection; /** Fetch an asset by its ID. */ asset?: Maybe; + /** Get an asset based on its currency code and scale if it exists. */ + assetByCodeAndScale?: Maybe; /** Fetch a paginated list of assets. */ assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ @@ -1136,6 +1138,8 @@ export type Query = { payments: PaymentConnection; /** Fetch a peer by its ID. */ peer?: Maybe; + /** Get a peer based on its ILP address and asset ID if it exists. */ + peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; /** Fetch an Open Payments quote by its ID. */ @@ -1144,6 +1148,8 @@ export type Query = { receiver?: Maybe; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; + /** Get a wallet address by its url if it exists */ + walletAddressByUrl?: Maybe; /** Fetch a paginated list of wallet addresses. */ walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ @@ -1162,6 +1168,12 @@ export type QueryAssetArgs = { }; +export type QueryAssetByCodeAndScaleArgs = { + code: Scalars['String']['input']; + scale: Scalars['UInt8']['input']; +}; + + export type QueryAssetsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1206,6 +1218,12 @@ export type QueryPeerArgs = { }; +export type QueryPeerByAddressAndAssetArgs = { + assetId: Scalars['String']['input']; + staticIlpAddress: Scalars['String']['input']; +}; + + export type QueryPeersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1230,6 +1248,11 @@ export type QueryWalletAddressArgs = { }; +export type QueryWalletAddressByUrlArgs = { + url: Scalars['String']['input']; +}; + + export type QueryWalletAddressesArgs = { after?: InputMaybe; before?: InputMaybe; @@ -2275,16 +2298,19 @@ export type PeersConnectionResolvers = { accountingTransfers?: Resolver>; asset?: Resolver, ParentType, ContextType, RequireFields>; + assetByCodeAndScale?: Resolver, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; peer?: Resolver, ParentType, ContextType, RequireFields>; + peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; + walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; }; diff --git a/packages/mock-account-service-lib/src/requesters.ts b/packages/mock-account-service-lib/src/requesters.ts index e6edd93bb9..2f79324190 100644 --- a/packages/mock-account-service-lib/src/requesters.ts +++ b/packages/mock-account-service-lib/src/requesters.ts @@ -12,7 +12,9 @@ import type { SetFeeResponse, FeeType, CreateOrUpdatePeerByUrlMutationResponse, - CreateOrUpdatePeerByUrlInput + CreateOrUpdatePeerByUrlInput, + Peer, + Asset } from './generated/graphql' import { v4 as uuid } from 'uuid' @@ -65,6 +67,12 @@ export function createRequesters( fixed: number, basisPoints: number ) => Promise + getAssetByCodeAndScale: (code: string, scale: number) => Promise + getWalletAddressByURL: (url: string) => Promise + getPeerByAddressAndAsset: ( + staticIlpAddress: string, + assetId: string + ) => Promise } { return { createAsset: (code, scale, liquidityThreshold) => @@ -104,7 +112,12 @@ export function createRequesters( createWalletAddressKey: ({ walletAddressId, jwk }) => createWalletAddressKey(apolloClient, logger, { walletAddressId, jwk }), setFee: (assetId, type, fixed, basisPoints) => - setFee(apolloClient, logger, assetId, type, fixed, basisPoints) + setFee(apolloClient, logger, assetId, type, fixed, basisPoints), + getAssetByCodeAndScale: (code, scale) => + getAssetByCodeAndScale(apolloClient, code, scale), + getWalletAddressByURL: (url) => getWalletAddressByURL(apolloClient, url), + getPeerByAddressAndAsset: (staticIlpAddress, assetId) => + getPeerByAddressAndAsset(apolloClient, staticIlpAddress, assetId) } } @@ -438,3 +451,90 @@ export async function setFee( return data.setFee }) } + +async function getAssetByCodeAndScale( + apolloClient: ApolloClient, + code: string, + scale: number +): Promise { + const getAssetQuery = gql` + query GetAssetByCodeAndScale($code: String!, $scale: UInt8!) { + assetByCodeAndScale(code: $code, scale: $scale) { + id + code + scale + liquidityThreshold + } + } + ` + const args = { code: code, scale: scale } + const { data } = await apolloClient.query({ + query: getAssetQuery, + variables: args + }) + + return data.assetByCodeAndScale +} + +async function getWalletAddressByURL( + apolloClient: ApolloClient, + url: string +): Promise { + const query = gql` + query getWalletAddressByUrl($url: String!) { + walletAddressByUrl(url: $url) { + id + liquidity + url + publicName + asset { + id + scale + code + withdrawalThreshold + } + } + } + ` + const { data } = await apolloClient.query({ + query: query, + variables: { url: url } + }) + + return data.walletAddressByUrl +} + +async function getPeerByAddressAndAsset( + apolloClient: ApolloClient, + staticIlpAddress: string, + assetId: string +): Promise { + const getPeerByAddressAndAssetQuery = gql` + query getPeerByAddressAndAsset( + $staticIlpAddress: String! + $assetId: String! + ) { + peerByAddressAndAsset( + staticIlpAddress: $staticIlpAddress + assetId: $assetId + ) { + id + name + asset { + id + scale + code + withdrawalThreshold + } + } + } + ` + const args = { staticIlpAddress: staticIlpAddress, assetId: assetId } + + const { data } = await apolloClient.query({ + query: getPeerByAddressAndAssetQuery, + variables: args + }) + + return data.peerByAddressAndAsset ?? null +} diff --git a/packages/mock-account-service-lib/src/seed.ts b/packages/mock-account-service-lib/src/seed.ts index eb1fc20515..6ccc50681a 100644 --- a/packages/mock-account-service-lib/src/seed.ts +++ b/packages/mock-account-service-lib/src/seed.ts @@ -37,13 +37,19 @@ export async function setupFromSeed( depositPeerLiquidity, createAutoPeer, createWalletAddress, - createWalletAddressKey + createWalletAddressKey, + getAssetByCodeAndScale, + getWalletAddressByURL, + getPeerByAddressAndAsset } = createRequesters(apolloClient, logger) const assets: Record = {} for (const { code, scale, liquidity, liquidityThreshold } of config.seed .assets) { - const { asset } = await createAsset(code, scale, liquidityThreshold) + let asset = await getAssetByCodeAndScale(code, scale) + if (!asset) { + asset = (await createAsset(code, scale, liquidityThreshold)).asset || null + } if (!asset) { throw new Error('asset not defined') } @@ -64,14 +70,20 @@ export async function setupFromSeed( const peerResponses = await Promise.all( config.seed.peers.map(async (peer: Peering) => { - const peerResponse = await createPeer( + let peerResponse = await getPeerByAddressAndAsset( peer.peerIlpAddress, - peer.peerUrl, - assets[peeringAsset].id, - assets[peeringAsset].code, - peer.name, - peer.liquidityThreshold - ).then((response) => response.peer) + assets[peeringAsset].id + ) + if (!peerResponse) { + peerResponse = await createPeer( + peer.peerIlpAddress, + peer.peerUrl, + assets[peeringAsset].id, + assets[peeringAsset].code, + peer.name, + peer.liquidityThreshold + ).then((response) => response.peer || null) + } if (!peerResponse) { throw new Error('peer response not defined') } @@ -126,11 +138,16 @@ export async function setupFromSeed( } logger.debug('hostname: ', config.publicHost) - const walletAddress = await createWalletAddress( - account.name, - `${config.publicHost}/${account.path}`, - accountAsset.id - ) + + const url = `${config.publicHost}/${account.path}` + let walletAddress = await getWalletAddressByURL(url) + if (!walletAddress) { + walletAddress = await createWalletAddress( + account.name, + url, + accountAsset.id + ) + } await mockAccounts.setWalletAddress( account.id, diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index 131de7eb44..9437d6ec24 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -1124,6 +1124,8 @@ export type Query = { accountingTransfers: AccountingTransferConnection; /** Fetch an asset by its ID. */ asset?: Maybe; + /** Get an asset based on its currency code and scale if it exists. */ + assetByCodeAndScale?: Maybe; /** Fetch a paginated list of assets. */ assets: AssetsConnection; /** Fetch an Open Payments incoming payment by its ID. */ @@ -1136,6 +1138,8 @@ export type Query = { payments: PaymentConnection; /** Fetch a peer by its ID. */ peer?: Maybe; + /** Get a peer based on its ILP address and asset ID if it exists. */ + peerByAddressAndAsset?: Maybe; /** Fetch a paginated list of peers. */ peers: PeersConnection; /** Fetch an Open Payments quote by its ID. */ @@ -1144,6 +1148,8 @@ export type Query = { receiver?: Maybe; /** Fetch a wallet address by its ID. */ walletAddress?: Maybe; + /** Get a wallet address by its url if it exists */ + walletAddressByUrl?: Maybe; /** Fetch a paginated list of wallet addresses. */ walletAddresses: WalletAddressesConnection; /** Fetch a paginated list of webhook events. */ @@ -1162,6 +1168,12 @@ export type QueryAssetArgs = { }; +export type QueryAssetByCodeAndScaleArgs = { + code: Scalars['String']['input']; + scale: Scalars['UInt8']['input']; +}; + + export type QueryAssetsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1206,6 +1218,12 @@ export type QueryPeerArgs = { }; +export type QueryPeerByAddressAndAssetArgs = { + assetId: Scalars['String']['input']; + staticIlpAddress: Scalars['String']['input']; +}; + + export type QueryPeersArgs = { after?: InputMaybe; before?: InputMaybe; @@ -1230,6 +1248,11 @@ export type QueryWalletAddressArgs = { }; +export type QueryWalletAddressByUrlArgs = { + url: Scalars['String']['input']; +}; + + export type QueryWalletAddressesArgs = { after?: InputMaybe; before?: InputMaybe; @@ -2275,16 +2298,19 @@ export type PeersConnectionResolvers = { accountingTransfers?: Resolver>; asset?: Resolver, ParentType, ContextType, RequireFields>; + assetByCodeAndScale?: Resolver, ParentType, ContextType, RequireFields>; assets?: Resolver>; incomingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayment?: Resolver, ParentType, ContextType, RequireFields>; outgoingPayments?: Resolver>; payments?: Resolver>; peer?: Resolver, ParentType, ContextType, RequireFields>; + peerByAddressAndAsset?: Resolver, ParentType, ContextType, RequireFields>; peers?: Resolver>; quote?: Resolver, ParentType, ContextType, RequireFields>; receiver?: Resolver, ParentType, ContextType, RequireFields>; walletAddress?: Resolver, ParentType, ContextType, RequireFields>; + walletAddressByUrl?: Resolver, ParentType, ContextType, RequireFields>; walletAddresses?: Resolver>; webhookEvents?: Resolver>; };