Skip to content

Commit

Permalink
feat: implement getContractMapEntry function (#1461)
Browse files Browse the repository at this point in the history
  • Loading branch information
zone117x authored Mar 17, 2023
1 parent 9d5b7fb commit 7031ead
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/network/src/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 70 additions & 1 deletion packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<T extends ClarityValue = ClarityValue>(
getContractMapEntryOptions: GetContractMapEntryOptions
): Promise<T | NoneCV> {
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<T>(responseJson.data);
} catch (error) {
throw new Error(`Error deserializing Clarity value "${responseJson.data}": ${error}`);
}
return deserializedCv;
}

/**
* Sponsored transaction options
*/
Expand Down
39 changes: 39 additions & 0 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
estimateTransaction,
estimateTransactionByteLength,
estimateTransactionFeeWithFallback,
getContractMapEntry,
getNonce,
makeContractCall,
makeContractDeploy,
Expand All @@ -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';
Expand Down Expand Up @@ -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<UIntCV>({
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);
});

1 comment on commit 7031ead

@vercel
Copy link

@vercel vercel bot commented on 7031ead Mar 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.