From 7031ead112f7333d165f5946eae0481f6aa9a20f Mon Sep 17 00:00:00 2001 From: Matthew Little Date: Fri, 17 Mar 2023 17:12:41 +0100 Subject: [PATCH] feat: implement `getContractMapEntry` function (#1461) --- packages/network/src/network.ts | 2 + packages/transactions/src/builders.ts | 71 ++++++++++++++++++++- packages/transactions/tests/builder.test.ts | 39 +++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/packages/network/src/network.ts b/packages/network/src/network.ts index 8fdeb861b..6d2ee8804 100644 --- a/packages/network/src/network.ts +++ b/packages/network/src/network.ts @@ -102,6 +102,8 @@ export class StacksNetwork { ${contractAddress}/${contractName}/get-stacker-info`; getDataVarUrl = (contractAddress: string, contractName: string, dataVarName: string) => `${this.coreApiUrl}/v2/data_var/${contractAddress}/${contractName}/${dataVarName}?proof=0`; + getMapEntryUrl = (contractAddress: string, contractName: string, mapName: string) => + `${this.coreApiUrl}/v2/map_entry/${contractAddress}/${contractName}/${mapName}?proof=0`; getNameInfo(fullyQualifiedName: string) { /* TODO: Update to v2 API URL for name lookups diff --git a/packages/transactions/src/builders.ts b/packages/transactions/src/builders.ts index a9b7e6cff..cbc4058b7 100644 --- a/packages/transactions/src/builders.ts +++ b/packages/transactions/src/builders.ts @@ -17,7 +17,7 @@ import { SpendingCondition, MultiSigSpendingCondition, } from './authorization'; -import { ClarityValue, PrincipalCV } from './clarity'; +import { ClarityValue, deserializeCV, NoneCV, PrincipalCV, serializeCV } from './clarity'; import { AddressHashMode, AddressVersion, @@ -1350,6 +1350,75 @@ export async function callReadOnlyFunction( return response.json().then(responseJson => parseReadOnlyResponse(responseJson)); } +export interface GetContractMapEntryOptions { + /** the contracts address */ + contractAddress: string; + /** the contracts name */ + contractName: string; + /** the map name */ + mapName: string; + /** key to lookup in the map */ + mapKey: ClarityValue; + /** the network that has the contract */ + network?: StacksNetworkName | StacksNetwork; +} + +/** + * Fetch data from a contract data map. + * @param getContractMapEntryOptions - the options object + * @returns + * Promise that resolves to a ClarityValue if the operation succeeds. + * Resolves to NoneCV if the map does not contain the given key, if the map does not exist, or if the contract prinicipal does not exist + */ +export async function getContractMapEntry( + getContractMapEntryOptions: GetContractMapEntryOptions +): Promise { + const defaultOptions = { + network: new StacksMainnet(), + }; + const { contractAddress, contractName, mapName, mapKey, network } = Object.assign( + defaultOptions, + getContractMapEntryOptions + ); + + const derivedNetwork = StacksNetwork.fromNameOrNetwork(network); + const url = derivedNetwork.getMapEntryUrl(contractAddress, contractName, mapName); + + const serializedKeyBytes = serializeCV(mapKey); + const serializedKeyHex = '0x' + bytesToHex(serializedKeyBytes); + + const fetchOptions: RequestInit = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify(serializedKeyHex), // endpoint expects a JSON string atom (quote wrapped string) + }; + + const response = await derivedNetwork.fetchFn(url, fetchOptions); + if (!response.ok) { + const msg = await response.text().catch(() => ''); + throw new Error( + `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${serializedKeyHex}". Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the message: "${msg}"` + ); + } + const responseBody = await response.text(); + const responseJson: { data?: string } = JSON.parse(responseBody); + if (!responseJson.data) { + throw new Error( + `Error fetching map entry for map "${mapName}" in contract "${contractName}" at address ${contractAddress}, using map key "${serializedKeyHex}". Response ${response.status}: ${response.statusText}. Attempted to fetch ${url} and failed with the response: "${responseBody}"` + ); + } + let deserializedCv: T; + try { + deserializedCv = deserializeCV(responseJson.data); + } catch (error) { + throw new Error(`Error deserializing Clarity value "${responseJson.data}": ${error}`); + } + return deserializedCv; +} + /** * Sponsored transaction options */ diff --git a/packages/transactions/tests/builder.test.ts b/packages/transactions/tests/builder.test.ts index 6ff9986d5..ca90851ad 100644 --- a/packages/transactions/tests/builder.test.ts +++ b/packages/transactions/tests/builder.test.ts @@ -24,6 +24,7 @@ import { estimateTransaction, estimateTransactionByteLength, estimateTransactionFeeWithFallback, + getContractMapEntry, getNonce, makeContractCall, makeContractDeploy, @@ -47,9 +48,11 @@ import { BytesReader } from '../src/bytesReader'; import { bufferCV, bufferCVFromString, + ClarityType, noneCV, serializeCV, standardPrincipalCV, + UIntCV, uintCV, } from '../src/clarity'; import { principalCV } from '../src/clarity/types/principalCV'; @@ -2084,3 +2087,39 @@ test('Call read-only function with network string', async () => { expect(fetchMock.mock.calls.length).toEqual(1); expect(result).toEqual(mockResult); }); + +test('Get contract map entry - success', async () => { + const mockValue = 60n; + const mockResult = uintCV(mockValue); + fetchMock.mockOnce(`{"data": "0x${bytesToHex(serializeCV(mockResult))}"}`); + + const result = await getContractMapEntry({ + contractAddress: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11', + contractName: 'newyorkcitycoin-core-v2', + mapName: 'UserIds', + mapKey: principalCV('SP25V8V2QQ2K8N3JAS15Z14W4YW7ABFDZHK5ZPGW7'), + }); + + expect(fetchMock.mock.calls.length).toEqual(1); + expect(result).toEqual(mockResult); + expect(result.type).toBe(ClarityType.UInt); + if (result.type === ClarityType.UInt) { + expect(result.value).toBe(mockValue); + } +}); + +test('Get contract map entry - no match', async () => { + const mockResult = noneCV(); + fetchMock.mockOnce(`{"data": "0x${bytesToHex(serializeCV(mockResult))}"}`); + + const result = await getContractMapEntry({ + contractAddress: 'SPSCWDV3RKV5ZRN1FQD84YE1NQFEDJ9R1F4DYQ11', + contractName: 'newyorkcitycoin-core-v2', + mapName: 'UserIds', + mapKey: principalCV('SP34EBMKMRR6SXX65GRKJ1FHEXV7AGHJ2D8ASQ5M3'), + }); + + expect(fetchMock.mock.calls.length).toEqual(1); + expect(result).toEqual(mockResult); + expect(result.type).toBe(ClarityType.OptionalNone); +});