Skip to content
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

Update Conditions API #267

Merged
merged 3 commits into from
Sep 5, 2023
Merged

Update Conditions API #267

merged 3 commits into from
Sep 5, 2023

Conversation

piotr-roslaniec
Copy link
Contributor

@piotr-roslaniec piotr-roslaniec commented Aug 21, 2023

Type of PR:

  • Feature

Required reviews:

  • 3

What this does:

  • Replaces Joi with Zod, for condition validation
  • Adds conditionType discriminating field to conditions
  • Exposes conditions interface, *Props

Issues fixed/closed:

Why it's needed:

  • Improves developer experience for TS users
  • Improves type-safety

Notes for reviewers:

  • Downstream change addressed in Add condition_type to condition schemas nucypher#3201
  • Marking this as a breaking change because of breaking changes in conditions API
  • Slightly changed the module structure - Condition is now at .conditions.Condition instead of .conditions.base.Condition
  • See how condition constructors are beings used now in conditions/base/*.test.ts
  • See how the type-hints work now in the screenshot below

image

@piotr-roslaniec piotr-roslaniec force-pushed the conditions-api branch 4 times, most recently from 758e94d to d9718ed Compare August 22, 2023 08:59
@nucypher nucypher deleted a comment from github-actions bot Aug 22, 2023
@nucypher nucypher deleted a comment from github-actions bot Aug 22, 2023
@nucypher nucypher deleted a comment from github-actions bot Aug 22, 2023
@@ -211,50 +212,6 @@ describe('condition set', () => {
}
);

it.each([
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed this test because we no longer duck-type conditions and rely on conditionType instead

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps you can repurpose to test to check what happens when the condition_type is either not defined, or defined but the JSON does not match what's expected based on the type.

const ownsBufficornNFT = ERC721Ownership.fromObj({
const ownsBufficornNFT = new ERC721Ownership({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Condition constructors are now taking objects

Copy link
Member

Choose a reason for hiding this comment

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

This is great 🙌

Comment on lines -68 to -71
export const bytesEquals = (first: Uint8Array, second: Uint8Array): boolean =>
first.length === second.length &&
first.every((value, index) => value === second[index]);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unused method

Copy link
Member

Choose a reason for hiding this comment

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

🪓

@@ -24,7 +24,6 @@ export { PolicyMessageKit } from './kits/message';
// Conditions
import type { CustomContextParam } from './conditions';
import * as conditions from './conditions';
// TODO: Not sure how to re-export this type from the conditions module
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Non-issue

Comment on lines +8 to +10
// TODO: Consider replacing with `z.unknown`:
// Since Solidity types are tied to Solidity version, we may not be able to accurately represent them in Zod.
// Alternatively, find a TS Solidity type lib.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RFC

Copy link
Contributor

Choose a reason for hiding this comment

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

Potentially use this?

https://abitype.dev/api/zod

Copy link
Contributor

Choose a reason for hiding this comment

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

Potentially use this?

https://abitype.dev/api/zod

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I think you could probably replace pretty much anything having to do with interfacing with contracts with this maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's likely. I've already started looking into viem: #271

.object({
name: z.string(),
type: z.enum(EthBaseTypes),
internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RFC

Copy link
Member

Choose a reason for hiding this comment

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

All this contract validation seems a little bit tricky, and I think there is a low probability of having a bad ABI contract. I would leave this as is.

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, it seems likely that ethers or viem could produce a useful error for the user if the abi isn't valid.

Comment on lines 42 to 54
private static conditionFromObject(obj: ConditionProps): Condition {
switch (obj.conditionType) {
case 'rpc':
return new RpcCondition(obj as RpcConditionProps);
case 'time':
return new TimeCondition(obj as TimeConditionProps);
case 'contract':
return new ContractCondition(obj as ContractConditionProps);
case 'compound':
return new CompoundCondition(obj as CompoundConditionProps);
default:
throw new Error(`Invalid conditionType: ${obj.conditionType}`);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

RFC: Do we really need access to concrete types now that we have the conditionType in every Condition? For example, what is there to gain for our users from having an RpcCondition instead of a Condition instance?

Copy link
Member

Choose a reason for hiding this comment

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

that's a good point. I like the simplicity of just having a condition instance.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm also +1 for having this simplified

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So just to reiterate: "I, as a TS developer, don't need to know the concrete type of my condition because I already know it's conditionType". Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

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

So just to reiterate: "I, as a TS developer, don't need to know the concrete type of my condition because I already know it's conditionType". Does that make sense?

Not sure I follow. What would be the outcome of such a plan?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me rephrase that:

  1. I just deserialized a condition. I want to know what type of condition it is. I can just check (by hand or programmatically) what is the value of conditionType. That's all good, the problem is solved.
  2. But now, why would I need to know the concrete TypeScript type of my conditions? And if I ever need to know that, I follow steps in 1) and cast my condition to the desired type, condition as RpcCondition (or any other condition, based on conditionType). So, essentially what the code above is doing.

Copy link
Contributor Author

@piotr-roslaniec piotr-roslaniec Aug 30, 2023

Choose a reason for hiding this comment

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

Move to Condition.fromObj

@piotr-roslaniec
Copy link
Contributor Author

Hi @ghardin1314, you may be interested in reviewing this PR.

@piotr-roslaniec piotr-roslaniec added the do not merge Open for review but do not merge please label Aug 22, 2023
@piotr-roslaniec
Copy link
Contributor Author

Wishing I used valibot instead. Perhaps in a different life.

Copy link
Member

@manumonti manumonti left a comment

Choose a reason for hiding this comment

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

This is a great job 👏. In really like this approach.

const ownsBufficornNFT = ERC721Ownership.fromObj({
const ownsBufficornNFT = new ERC721Ownership({
Copy link
Member

Choose a reason for hiding this comment

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

This is great 🙌

Comment on lines -68 to -71
export const bytesEquals = (first: Uint8Array, second: Uint8Array): boolean =>
first.length === second.length &&
first.every((value, index) => value === second[index]);

Copy link
Member

Choose a reason for hiding this comment

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

🪓

.object({
name: z.string(),
type: z.enum(EthBaseTypes),
internalType: z.enum(EthBaseTypes), // TODO: Do we need to validate this?
Copy link
Member

Choose a reason for hiding this comment

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

All this contract validation seems a little bit tricky, and I think there is a low probability of having a bad ABI contract. I would leave this as is.

Comment on lines 42 to 54
private static conditionFromObject(obj: ConditionProps): Condition {
switch (obj.conditionType) {
case 'rpc':
return new RpcCondition(obj as RpcConditionProps);
case 'time':
return new TimeCondition(obj as TimeConditionProps);
case 'contract':
return new ContractCondition(obj as ContractConditionProps);
case 'compound':
return new CompoundCondition(obj as CompoundConditionProps);
default:
throw new Error(`Invalid conditionType: ${obj.conditionType}`);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

that's a good point. I like the simplicity of just having a condition instance.

src/conditions/base/rpc.ts Outdated Show resolved Hide resolved
src/conditions/base/rpc.ts Outdated Show resolved Hide resolved
src/conditions/base/rpc.ts Outdated Show resolved Hide resolved
src/conditions/base/rpc.ts Outdated Show resolved Hide resolved
src/conditions/base/rpc.ts Outdated Show resolved Hide resolved
src/conditions/compound-condition.ts Show resolved Hide resolved
Comment on lines 42 to 54
private static conditionFromObject(obj: ConditionProps): Condition {
switch (obj.conditionType) {
case 'rpc':
return new RpcCondition(obj as RpcConditionProps);
case 'time':
return new TimeCondition(obj as TimeConditionProps);
case 'contract':
return new ContractCondition(obj as ContractConditionProps);
case 'compound':
return new CompoundCondition(obj as CompoundConditionProps);
default:
throw new Error(`Invalid conditionType: ${obj.conditionType}`);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

So just to reiterate: "I, as a TS developer, don't need to know the concrete type of my condition because I already know it's conditionType". Does that make sense?

Not sure I follow. What would be the outcome of such a plan?

test/unit/conditions/base/condition.test.ts Outdated Show resolved Hide resolved
@@ -211,50 +212,6 @@ describe('condition set', () => {
}
);

it.each([
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps you can repurpose to test to check what happens when the condition_type is either not defined, or defined but the JSON does not match what's expected based on the type.

src/conditions/condition.ts Show resolved Hide resolved
@codecov-commenter
Copy link

codecov-commenter commented Aug 30, 2023

Codecov Report

Merging #267 (84c8ef4) into alpha (a78bc93) will increase coverage by 0.36%.
The diff coverage is 96.96%.

@@            Coverage Diff             @@
##            alpha     #267      +/-   ##
==========================================
+ Coverage   80.63%   80.99%   +0.36%     
==========================================
  Files          36       36              
  Lines        1012     1021       +9     
  Branches      112      110       -2     
==========================================
+ Hits          816      827      +11     
+ Misses        188      186       -2     
  Partials        8        8              
Files Changed Coverage Δ
src/conditions/context/context.ts 94.73% <ø> (ø)
src/utils.ts 90.90% <ø> (-2.71%) ⬇️
src/conditions/zod.ts 83.33% <83.33%> (ø)
src/conditions/base/contract.ts 95.45% <95.00%> (+16.14%) ⬆️
src/conditions/condition.ts 97.36% <97.36%> (ø)
src/agents/contracts.ts 47.05% <100.00%> (ø)
src/conditions/base/index.ts 100.00% <100.00%> (ø)
src/conditions/base/rpc.ts 100.00% <100.00%> (ø)
src/conditions/base/shared.ts 100.00% <100.00%> (ø)
src/conditions/base/time.ts 100.00% <100.00%> (ø)
... and 8 more

src/types.ts Outdated
@@ -8,3 +8,10 @@ export enum ChainId {
GOERLI = 5,
MAINNET = 1,
}

export const SUPPORTED_CHAIN_IDS = [
Copy link
Member

Choose a reason for hiding this comment

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

Is SUPORTED_CHAIN_IDS used outstide of conditions? If so, it could be good to put it back in const.

Same question for ChainId.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved SUPORTED_CHAIN_IDS to const.ts and ChainId to web3.ts

src/zod.ts Outdated
@@ -0,0 +1,31 @@
import { Primitive, z, ZodLiteral } from 'zod';
Copy link
Member

Choose a reason for hiding this comment

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

Nice. Should we move this to the conditions directory? Unless Zod is used/will be used somewhere else?

});
}).toThrow(`Invalid conditionType: ${invalidConditionType}`);
}
);
Copy link
Member

Choose a reason for hiding this comment

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

Assuming not already tested somewhere else, perhaps you can also test providing a legit condition type, but the values provided don't match that type - is that possible?

For example, specify a time condition type, but provide valid contract condition values.

}
}

export {
Copy link
Member

Choose a reason for hiding this comment

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

Do the individual base schamas and props need to be exported, or is the specific condition classes (above) sufficient.

Copy link
Contributor Author

@piotr-roslaniec piotr-roslaniec Aug 31, 2023

Choose a reason for hiding this comment

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

I think props yes (because they make typing a bit easier), schemas - not so much (no direct value-add for users).

public readonly schema: ConditionSchema,
public readonly value: ConditionProps
) {
const { data, error } = Condition.validate(schema, value);
Copy link
Member

Choose a reason for hiding this comment

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

👍

public static validate(
schema: ConditionSchema,
value: ConditionProps,
override: Partial<ConditionProps> = {}
Copy link
Member

Choose a reason for hiding this comment

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

Do we need the override? I assumed (perhaps mistakenly so) that this would match the constructor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmmm I don't think we need those anymore

@piotr-roslaniec piotr-roslaniec force-pushed the conditions-api branch 2 times, most recently from f0b0b56 to 8ec9151 Compare September 4, 2023 10:45
@github-actions
Copy link

github-actions bot commented Sep 4, 2023

Bundled size for the package is listed below:

build/main/src/characters: 74.22 KB
build/main/src/kits: 19.53 KB
build/main/src/conditions/context: 42.97 KB
build/main/src/conditions/predefined: 19.53 KB
build/main/src/conditions/base: 54.69 KB
build/main/src/conditions: 175.78 KB
build/main/src/agents: 39.06 KB
build/main/src/sdk/strategy: 35.16 KB
build/main/src/sdk: 46.88 KB
build/main/src/policies: 19.53 KB
build/main/src: 453.13 KB
build/main/types/ethers-contracts/factories: 82.03 KB
build/main/types/ethers-contracts: 152.34 KB
build/main/types: 156.25 KB
build/main: 667.97 KB
build/module/src/characters: 78.13 KB
build/module/src/kits: 19.53 KB
build/module/src/conditions/context: 42.97 KB
build/module/src/conditions/predefined: 19.53 KB
build/module/src/conditions/base: 54.69 KB
build/module/src/conditions: 175.78 KB
build/module/src/agents: 39.06 KB
build/module/src/sdk/strategy: 31.25 KB
build/module/src/sdk: 42.97 KB
build/module/src/policies: 19.53 KB
build/module/src: 449.22 KB
build/module/types/ethers-contracts/factories: 82.03 KB
build/module/types/ethers-contracts: 152.34 KB
build/module/types: 156.25 KB
build/module: 664.06 KB
build: 1.30 MB

Copy link
Member

@arjunhassard arjunhassard left a comment

Choose a reason for hiding this comment

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

Nice work @piotr-roslaniec
Thank you @ghardin1314 for the DX improvements

expect(result.error).toBeUndefined();
expect(result.data).toEqual({
...conditionObj,
conditionType: 'compound',
Copy link
Member

Choose a reason for hiding this comment

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

For the documentation and Conditions Schema, should we consider compound to be a fourth condition type, or on a different 'level' from contract, rpc and time? And does the constructor tell the developer that they're creating a compound condition set?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think CompoundCondition is somewhat different since (by definition) it can't be used on its own. That's why I didn't put it into conditions/base which is supposed to contain some basic building blocks for conditions.

Yes, the constructor expects CompoundConditionProps and provides type hints for TS developers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do not merge Open for review but do not merge please
Projects
Status: Completed
Development

Successfully merging this pull request may close these issues.

7 participants