Skip to content

Feat/dynamic context validation in sdk #1703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
dbb89d2
feat(sdk): dynamic-context-validation
tractorss Oct 8, 2024
85f3668
chore(web): add-graph-api-key-env
tractorss Oct 8, 2024
cdbbcf2
feat(subgraph): fetch-dispute-request-event-data-within-subgraph
tractorss Oct 11, 2024
c59e102
feat(kleros-sdk): get-dispute-function
tractorss Oct 14, 2024
220ea2f
chore(web): update-dispute-population-flow
tractorss Oct 14, 2024
6974fdb
chore(web): configure-sdk-with-web3-context
tractorss Oct 14, 2024
88963e7
chore(kleros-sdk): make-configuring-sdk-explicit
tractorss Oct 14, 2024
3ad42d5
refactor(kleros-sdk): implement-rabbit-ai-feedback
tractorss Oct 14, 2024
d2c8f9c
Merge branch 'dev' into feat/dynamic-context-validation-in-sdk
tractorss Oct 15, 2024
25063c7
chore: fixed the SDK tests, minor tweaks
jaybuidl Oct 15, 2024
3abfbc5
fix(kleros-sdk): public-client-null-check
tractorss Oct 15, 2024
9140518
chore(subgraph): redeploy-subgraphs
tractorss Oct 15, 2024
e8cd12d
fix(sdk): types and unit tests
jaybuidl Oct 15, 2024
d3e4b59
chore(sdk): release configuration for NPM, tsconfig tweaks
jaybuidl Oct 16, 2024
1a11c84
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 16, 2024
98c2907
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 16, 2024
b851f2a
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 16, 2024
57711ba
docs(sdk): readme
jaybuidl Oct 16, 2024
cbdd6d1
chore: clean up
jaybuidl Oct 16, 2024
77b57e4
chore(kleros-sdk): define-entry-points-for-files
tractorss Oct 16, 2024
71f851b
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 16, 2024
ecc9edf
refactor(kleros-sdk): remove-path-aliasing
tractorss Oct 18, 2024
e698548
refactor(kleros-sdk): update-get-dispute-function-parameter-type
tractorss Oct 21, 2024
93e59a1
fix(web): typing
tractorss Oct 21, 2024
18ae12f
Merge branch 'dev' into feat/dynamic-context-validation-in-sdk
tractorss Oct 21, 2024
0a8422f
chore: update-yarn-lock
tractorss Oct 21, 2024
04c80db
chore(kleros-sdk): update-get-dispute-id-spec
tractorss Oct 21, 2024
9b4e9d2
feat(kleros-sdk): better-error-handling-and-optimisations
tractorss Oct 22, 2024
7d5ed21
refactor(kleros-sdk): sonar-cloud-fixes
tractorss Oct 22, 2024
542a8d9
refactor(kleros-sdk): remoev-unused-import
tractorss Oct 22, 2024
3d42edc
refactor(kleros-sdk): address-coderabbit-feedback
tractorss Oct 22, 2024
a387e77
refactor(kleros-sdk): refactor-error-classes
tractorss Oct 22, 2024
56853b9
fix: test mocks
jaybuidl Oct 23, 2024
52b31a3
fix(kleros-sdk): replace-graphql-request-library-with-native-fetch
tractorss Oct 23, 2024
cab784b
chore(kleros-sdk): use-urql-for-gql-queries
tractorss Oct 24, 2024
0562712
feat(kleros-sdk): gql-client-caching
tractorss Oct 24, 2024
078b233
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 25, 2024
128e1e5
chore(sdk): publish script
jaybuidl Oct 25, 2024
d2cb260
chore(sdk): release @kleros/[email protected]
jaybuidl Oct 25, 2024
7b2ccd3
Merge branch 'dev' into feat/dynamic-context-validation-in-sdk
jaybuidl Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kleros-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"dependencies": {
"@kleros/kleros-v2-contracts": "workspace:^",
"@reality.eth/reality-eth-lib": "^3.2.30",
"graphql-request": "^7.1.0",
"mustache": "^4.2.0",
"zod": "^3.22.4"
}
Expand Down
5 changes: 2 additions & 3 deletions kleros-sdk/src/dataMappings/actions/callAction.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { parseAbiItem } from "viem";
import { AbiCallMapping } from "src/dataMappings/utils/actionTypes";
import { createResultObject } from "src/dataMappings/utils/createResultObject";
import { configureSDK, getPublicClient } from "src/sdk";
import { getPublicClient } from "src/sdk";

export const callAction = async (mapping: AbiCallMapping, alchemyApiKey: string) => {
configureSDK({ apiKey: alchemyApiKey });
export const callAction = async (mapping: AbiCallMapping) => {
const publicClient = getPublicClient();

const { abi: source, address, args, seek, populate } = mapping;
Expand Down
5 changes: 2 additions & 3 deletions kleros-sdk/src/dataMappings/actions/eventAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import { parseAbiItem } from "viem";
import { type AbiEvent } from "abitype";
import { AbiEventMapping } from "src/dataMappings/utils/actionTypes";
import { createResultObject } from "src/dataMappings/utils/createResultObject";
import { configureSDK, getPublicClient } from "src/sdk";
import { getPublicClient } from "src/sdk";

export const eventAction = async (mapping: AbiEventMapping, alchemyApiKey: string) => {
configureSDK({ apiKey: alchemyApiKey });
export const eventAction = async (mapping: AbiEventMapping) => {
const publicClient = getPublicClient();

const { abi: source, address, eventFilter, seek, populate } = mapping;
Expand Down
4 changes: 2 additions & 2 deletions kleros-sdk/src/dataMappings/executeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ export const executeAction = async (
case "json":
return jsonAction(validateJsonMapping(mapping));
case "abi/call":
return await callAction(validateAbiCallMapping(mapping), context.alchemyApiKey as string);
return await callAction(validateAbiCallMapping(mapping));
case "abi/event":
return await eventAction(validateAbiEventMapping(mapping), context.alchemyApiKey as string);
return await eventAction(validateAbiEventMapping(mapping));
case "fetch/ipfs/json":
return await fetchIpfsJsonAction(validateFetchIpfsJsonMapping(mapping));
case "reality":
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import mustache from "mustache";
import retrieveVariables from "./retrieveVariables";
import { ActionMapping } from "./actionTypes";

export function replacePlaceholdersWithValues(
Expand All @@ -7,6 +8,7 @@ export function replacePlaceholdersWithValues(
): ActionMapping | ActionMapping[] {
function replace(obj: ActionMapping): ActionMapping | ActionMapping[] {
if (typeof obj === "string") {
validateContext(obj, context);
return mustache.render(obj, context) as unknown as ActionMapping;
} else if (Array.isArray(obj)) {
return obj.map(replace) as unknown as ActionMapping[];
Expand All @@ -21,3 +23,18 @@ export function replacePlaceholdersWithValues(

return replace(mapping);
}

/**
*
* @param template
* @param context
* @description retrieves all variables from a template and validates if they are provided in the context
*/
const validateContext = (template: string, context: Record<string, unknown>) => {
const variables = retrieveVariables(template);

variables.forEach((variable) => {
if (!context[variable]) throw new Error(`Expected key : "${variable}" to be provided in context.`);
});
return true;
};
20 changes: 20 additions & 0 deletions kleros-sdk/src/dataMappings/utils/retrieveVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import mustache from "mustache";

/**
*
* @param template
* @returns Variables[] returns the variables in a template string
* @note This is a naive implementation and wil not work for complex tags, only works for {{}} and {{{}}} tags for now.
* Reference : https://github.com/janl/mustache.js/issues/538
*/
const retrieveVariables = (template: string) =>
mustache
.parse(template)
.filter(function (v) {
return v[0] === "name" || v[0] === "&";
}) // add more conditions here to include more tags
.map(function (v) {
return v[1];
});

export default retrieveVariables;
35 changes: 35 additions & 0 deletions kleros-sdk/src/requests/fetchDisputeDetails.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { request } from "graphql-request";

type DisputeDetailsQueryResponse = {
dispute: {
arbitrated: {
id: string;
};
arbitrableChainId: number;
externalDisputeId: number;
templateId: number;
};
};

const fetchDisputeDetails = async (endpoint: string, id: number) => {
const query = `
query DisputeDetails {
dispute(id: ${id}) {
arbitrated {
id
}
arbitrableChainId
externalDisputeId
templateId
}
}
`;

try {
return await request<DisputeDetailsQueryResponse>(endpoint, query);
} catch (error: any) {
throw new Error(`Error querying Dispute Details , endpoint : ${endpoint}, message : ${error?.message}`);
}
};

export default fetchDisputeDetails;
27 changes: 27 additions & 0 deletions kleros-sdk/src/requests/fetchDisputeTemplateFromId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { request } from "graphql-request";

type DisputeTemplateQueryResponse = {
disputeTemplate: {
templateData: string;
templateDataMappings: string;
};
};

const fetchDisputeTemplateFromId = async (endpoint: string, id: number) => {
const query = `
query DisputeTemplate {
disputeTemplate(id: ${id}) {
templateData
templateDataMappings
}
}
`;

try {
return await request<DisputeTemplateQueryResponse>(endpoint, query);
} catch (error: any) {
throw new Error(`Error querying Dispute Template Registry , endpoint : ${endpoint}, message : ${error?.message}`);
}
};

export default fetchDisputeTemplateFromId;
17 changes: 6 additions & 11 deletions kleros-sdk/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { createPublicClient, webSocket, type PublicClient } from "viem";
import { arbitrumSepolia } from "viem/chains";
import { createPublicClient, type PublicClient } from "viem";
import { SdkConfig } from "./types";

let publicClient: PublicClient | undefined;

export const configureSDK = (config: { apiKey?: string }) => {
if (config.apiKey) {
const ALCHEMY_API_KEY = config.apiKey;
const transport = webSocket(`wss://arb-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`);
publicClient = createPublicClient({
chain: arbitrumSepolia,
transport,
});
export const configureSDK = (config: SdkConfig) => {
if (config.client) {
publicClient = createPublicClient(config.client);
}
};

export const getPublicClient = (): PublicClient => {
export const getPublicClient = (): PublicClient | undefined => {
if (!publicClient) {
throw new Error("SDK not configured. Please call `configureSDK` before using.");
}
Expand Down
17 changes: 17 additions & 0 deletions kleros-sdk/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PublicClientConfig } from "viem";

export type SdkConfig = {
client: PublicClientConfig;
};

type GetDisputeParametersOptions = {
sdkConfig: SdkConfig;
additionalContext: Record<string, any>;
};

export type GetDisputeParameters = {
disputeId: number;
coreSubgraph: string;
dtrSubgraph: string;
options: GetDisputeParametersOptions | undefined;
};
57 changes: 57 additions & 0 deletions kleros-sdk/src/utils/getDispute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { type GetDisputeParameters } from "src/types";
import { configureSDK } from "src/sdk";
import fetchDisputeDetails from "src/requests/fetchDisputeDetails";
import fetchDisputeTemplateFromId from "src/requests/fetchDisputeTemplateFromId";
import { executeActions } from "dataMappings/executeActions";
import { populateTemplate } from "dataMappings/utils/populateTemplate";
import { DisputeDetails } from "dataMappings/utils/disputeDetailsTypes";

/**
* Retrieves dispute parameters based on the provided dispute ID and subgraph endpoints.
*
* @param {GetDisputeParameters} disputeParameters - The parameters required to get the dispute.
* @param {number} disputeParameters.disputeId - A unique numeric identifier of the dispute in the Kleros Core contract.
* @param {string} disputeParameters.coreSubgraph - Endpoint for the Kleros core subgraph to use.
* @param {string} disputeParameters.dtrSubgraph - Endpoint for the Kleros dispute template registry subgraph.
* @param {GetDisputeParametersOptions | undefined} disputeParameters.options - Optional parameters to configure the SDK and provide additional context, if not configured already.
*/
export const getDispute = async (disputeParameters: GetDisputeParameters): Promise<DisputeDetails | undefined> => {
if (disputeParameters.options?.sdkConfig) {
configureSDK(disputeParameters.options.sdkConfig);
}
const { disputeId, dtrSubgraph, coreSubgraph, options } = disputeParameters;

const disputeDetails = await fetchDisputeDetails(coreSubgraph, disputeId);

if (!disputeDetails?.dispute) {
throw new Error(`Dispute details not found for disputeId: ${disputeId}`);
}

const template = await fetchDisputeTemplateFromId(dtrSubgraph, disputeDetails.dispute.templateId);

if (!template) {
throw new Error(`Template not found for template ID: ${disputeDetails.dispute.templateId}`);
}

const { templateData, templateDataMappings } = template.disputeTemplate;

const initialContext = {
arbitrableAddress: disputeDetails.dispute.arbitrated.id,
arbitrableChainID: disputeDetails.dispute.arbitrableChainId,
externalDisputeID: disputeDetails.dispute.externalDisputeId,
...options?.additionalContext,
};

let data = {};
if (templateDataMappings) {
try {
data = await executeActions(JSON.parse(templateDataMappings), initialContext);
} catch (err: any) {
throw new Error(err);
}
}

const populatedTemplate = populateTemplate(templateData, data);

return populatedTemplate;
};
50 changes: 42 additions & 8 deletions kleros-sdk/test/dataMappings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ describe("full flow test", () => {
{ title: "Yes", description: "User is responsible", id: "0x01" },
{ title: "No", description: "User is not responsible", id: "0x02" },
],
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
details: {
ruling: "{{ruling}}",
tied: "{{tied}}",
Expand All @@ -182,6 +183,9 @@ describe("full flow test", () => {
toAddress: "{{toAddress}}",
transferValue: "{{transferValue}}",
},
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
version: "1.0",
});

const initialContext = { alchemyApiKey: "mocked_api_key" };
Expand All @@ -199,6 +203,7 @@ describe("full flow test", () => {
{ title: "Yes", description: "User is responsible", id: "0x01" },
{ title: "No", description: "User is not responsible", id: "0x02" },
],
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
details: {
ruling: "1",
tied: "false",
Expand All @@ -207,6 +212,9 @@ describe("full flow test", () => {
toAddress: "0x0987654321098765432109876543210987654321",
transferValue: "100",
},
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
version: "1.0",
});
});
});
Expand Down Expand Up @@ -345,10 +353,7 @@ describe("populateTemplate", () => {
reserved: false,
},
],
policyURI: "https://example.com/policy",
frontendUrl: "https://example.com",
arbitrableChainID: "100",
arbitrableAddress: "0x1234567890123456789012345678901234567890",
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
category: "General",
Expand Down Expand Up @@ -376,10 +381,7 @@ describe("populateTemplate", () => {
reserved: false,
},
],
policyURI: "https://example.com/policy",
frontendUrl: "https://example.com",
arbitrableChainID: "100",
arbitrableAddress: "0x1234567890123456789012345678901234567890",
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
category: "General",
Expand All @@ -394,6 +396,22 @@ describe("populateTemplate", () => {
title: "Test Title",
description: "Test Description",
question: "{{missingQuestion}}",
type: "single-select",
answers: [
{
title: "Yes",
description: "Affirmative",
id: "0x01",
reserved: false,
},
],
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
category: "General",
lang: "en_US",
specification: "Spec",
version: "1.0",
});

const data = {
Expand All @@ -406,6 +424,22 @@ describe("populateTemplate", () => {
title: "Test Title",
description: "Test Description",
question: "",
type: "single-select",
answers: [
{
title: "Yes",
description: "Affirmative",
id: "0x01",
reserved: false,
},
],
policyURI: "/ipfs/QmUnPyGi31RoF4DRR8vT3u13YsppxtsbBKbdQAbcP8be4M/file.json",
arbitratorChainID: "421613",
arbitratorAddress: "0x0987654321098765432109876543210987654321",
category: "General",
lang: "en_US",
specification: "Spec",
version: "1.0",
});
});

Expand Down
4 changes: 4 additions & 0 deletions subgraph/core/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ type Dispute @entity {
jurors: [User!]! @derivedFrom(field: "disputes")
shifts: [TokenAndETHShift!]! @derivedFrom(field: "dispute")
disputeKitDispute: [DisputeKitDispute!]! @derivedFrom(field: "coreDispute")
isCrossChain: Boolean
arbitrableChainId:BigInt
externalDisputeId:BigInt
templateId:BigInt
}

type PeriodIndexCounter @entity {
Expand Down
Loading