Skip to content

Commit 02762f8

Browse files
authored
Add ContextVariableCondition with flexible variable matching (#688)
2 parents e8f2974 + 526c63f commit 02762f8

File tree

16 files changed

+395
-354
lines changed

16 files changed

+395
-354
lines changed

packages/taco/examples/conditions.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -124,26 +124,3 @@ console.assert(
124124
myContractCallCondition.requiresAuthentication(),
125125
'ContractCondition requires a signer',
126126
);
127-
128-
// Address Allowlist Condition Example
129-
const addressAllowlistCondition =
130-
new conditions.base.addressAllowlist.AddressAllowlistCondition({
131-
userAddress: ':userAddress',
132-
addresses: [
133-
'0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
134-
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
135-
'0x0000000000000000000000000000000000000001',
136-
],
137-
});
138-
139-
console.assert(
140-
addressAllowlistCondition.requiresAuthentication(),
141-
'AddressAllowlistCondition requires authentication',
142-
);
143-
144-
// You can check if an address is allowed with the condition's toObj method
145-
const addressToCheck = '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77';
146-
const addresses = addressAllowlistCondition.toObj().addresses;
147-
// This would be checked by taco nodes after validating the wallet signature provided by encryptor (the value of the variable `addressToCheck`).
148-
const isAllowed = addresses.includes(addressToCheck);
149-
console.assert(isAllowed, 'Address should be allowed but it is not.');

packages/taco/integration-test/condition-lingo.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from 'axios';
22
import * as https from 'https';
33
import { describe, test } from 'vitest';
4+
import { conditions } from '../src';
45
import {
56
CompoundCondition,
67
CompoundConditionType,
@@ -14,6 +15,7 @@ import {
1415
} from '../src/conditions/schemas/ecdsa';
1516
import { SequentialCondition } from '../src/conditions/sequential';
1617
import {
18+
testContextVariableConditionObj,
1719
testContractConditionObj,
1820
testJsonApiConditionObj,
1921
testJsonRpcConditionObj,
@@ -162,5 +164,14 @@ describe.skipIf(!process.env.RUNNING_IN_CI)(
162164
await validateConditionExpression(conditionExpr);
163165
}
164166
}, 15000);
167+
168+
test('validate context variable condition structure', async () => {
169+
const contextVariableCondition =
170+
new conditions.base.contextVariable.ContextVariableCondition(
171+
testContextVariableConditionObj,
172+
);
173+
const conditionExpr = new ConditionExpression(contextVariableCondition);
174+
await validateConditionExpression(conditionExpr);
175+
}, 15000);
165176
},
166177
);

packages/taco/integration-test/encrypt-decrypt.test.ts

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CompoundCondition } from '../src/conditions/compound-condition';
1818
import {
1919
createSignatureForTestSecp256k1ECDSACondition,
2020
createTestSecp256k1ECDSACondition,
21+
testContextVariableConditionObj,
2122
UINT256_MAX,
2223
} from '../test/test-utils';
2324

@@ -123,58 +124,6 @@ describe.skipIf(!process.env.RUNNING_IN_CI)(
123124
expect(decryptedMessageString).toEqual(messageString);
124125
}, 15000);
125126

126-
test('should encrypt and decrypt according to wallet allowlist condition', async () => {
127-
const messageString =
128-
'This message should only be accessible to allowed wallet addresses';
129-
const message = toBytes(messageString);
130-
131-
const addressAllowlistCondition =
132-
new conditions.base.addressAllowlist.AddressAllowlistCondition({
133-
userAddress: ':userAddress',
134-
addresses: [
135-
CONSUMER_ADDRESS,
136-
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
137-
'0x0000000000000000000000000000000000000001',
138-
],
139-
});
140-
141-
expect(addressAllowlistCondition.requiresAuthentication()).toBe(true);
142-
143-
const messageKit = await encrypt(
144-
provider,
145-
DOMAIN,
146-
message,
147-
addressAllowlistCondition,
148-
RITUAL_ID,
149-
encryptorSigner,
150-
);
151-
152-
const encryptedBytes = messageKit.toBytes();
153-
154-
const messageKitFromBytes = ThresholdMessageKit.fromBytes(encryptedBytes);
155-
const conditionContext =
156-
conditions.context.ConditionContext.fromMessageKit(messageKitFromBytes);
157-
158-
const authProvider = new EIP4361AuthProvider(provider, consumerSigner, {
159-
domain: 'localhost',
160-
uri: 'http://localhost:3000',
161-
});
162-
conditionContext.addAuthProvider(
163-
USER_ADDRESS_PARAM_DEFAULT,
164-
authProvider,
165-
);
166-
167-
const decryptedBytes = await decrypt(
168-
provider,
169-
DOMAIN,
170-
messageKitFromBytes,
171-
conditionContext,
172-
);
173-
const decryptedMessageString = fromBytes(decryptedBytes);
174-
175-
expect(decryptedMessageString).toEqual(messageString);
176-
}, 15000);
177-
178127
test('should encrypt and decrypt according to ECDSA signature condition with predefined verifying key', async () => {
179128
const messageString =
180129
'This message is protected by ECDSA signature verification 🔐';
@@ -329,5 +278,106 @@ describe.skipIf(!process.env.RUNNING_IN_CI)(
329278

330279
expect(decryptedMessageString).toEqual(messageString);
331280
}, 25000);
281+
282+
test('should encrypt and decrypt with ContextVariableCondition', async () => {
283+
const messageString =
284+
'This message is protected by context variable condition 🔐';
285+
const message = toBytes(messageString);
286+
287+
// Create a context variable condition that checks if userAddress is in allowlist
288+
const contextVariableCondition = new conditions.base.contextVariable.ContextVariableCondition({
289+
...testContextVariableConditionObj,
290+
returnValueTest: {
291+
comparator: 'in',
292+
value: [
293+
CONSUMER_ADDRESS.toLowerCase(),
294+
'0x0000000000000000000000000000000000000001',
295+
],
296+
},
297+
});
298+
299+
expect(contextVariableCondition.requiresAuthentication()).toBe(true);
300+
301+
const messageKit = await encrypt(
302+
provider,
303+
DOMAIN,
304+
message,
305+
contextVariableCondition,
306+
RITUAL_ID,
307+
encryptorSigner,
308+
);
309+
310+
const encryptedBytes = messageKit.toBytes();
311+
312+
const messageKitFromBytes = ThresholdMessageKit.fromBytes(encryptedBytes);
313+
const conditionContext =
314+
conditions.context.ConditionContext.fromMessageKit(messageKitFromBytes);
315+
316+
expect(
317+
conditionContext.requestedContextParameters.has(
318+
USER_ADDRESS_PARAM_DEFAULT,
319+
),
320+
).toBeTruthy();
321+
322+
// Add auth provider for userAddress context parameter
323+
const authProvider = new EIP4361AuthProvider(provider, consumerSigner);
324+
conditionContext.addAuthProvider(
325+
USER_ADDRESS_PARAM_DEFAULT,
326+
authProvider,
327+
);
328+
329+
const decryptedBytes = await decrypt(
330+
provider,
331+
DOMAIN,
332+
messageKitFromBytes,
333+
conditionContext,
334+
);
335+
const decryptedMessageString = fromBytes(decryptedBytes);
336+
337+
expect(decryptedMessageString).toEqual(messageString);
338+
}, 20000);
339+
340+
test('should fail to decrypt with ContextVariableCondition when userAddress is not in allowlist', async () => {
341+
const messageString = 'This should fail with wrong address';
342+
const message = toBytes(messageString);
343+
344+
// Create a context variable condition with specific allowed addresses
345+
const restrictedContextVariableCondition = new conditions.base.contextVariable.ContextVariableCondition({
346+
...testContextVariableConditionObj,
347+
returnValueTest: {
348+
comparator: 'in',
349+
value: [
350+
'0x0000000000000000000000000000000000000001', // Not our consumer address
351+
'0x0000000000000000000000000000000000000002',
352+
],
353+
},
354+
});
355+
356+
const messageKit = await encrypt(
357+
provider,
358+
DOMAIN,
359+
message,
360+
restrictedContextVariableCondition,
361+
RITUAL_ID,
362+
encryptorSigner,
363+
);
364+
365+
const encryptedBytes = messageKit.toBytes();
366+
367+
const messageKitFromBytes = ThresholdMessageKit.fromBytes(encryptedBytes);
368+
const conditionContext =
369+
conditions.context.ConditionContext.fromMessageKit(messageKitFromBytes);
370+
371+
// Add auth provider for userAddress context parameter (but with wrong address)
372+
const authProvider = new EIP4361AuthProvider(provider, consumerSigner);
373+
conditionContext.addAuthProvider(
374+
USER_ADDRESS_PARAM_DEFAULT,
375+
authProvider,
376+
);
377+
378+
await expect(
379+
decrypt(provider, DOMAIN, messageKitFromBytes, conditionContext),
380+
).rejects.toThrow();
381+
}, 20000);
332382
},
333383
);

packages/taco/schema-docs/condition-schemas.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ _Union of the following possible types:_
66

77
- [RpcCondition](#rpccondition)
88
- [TimeCondition](#timecondition)
9-
- [AddressAllowlistCondition](#addressallowlistcondition)
9+
- [ContextVariableCondition](#contextvariablecondition)
1010
- [ContractCondition](#contractcondition)
1111
- [EcdsaCondition](#ecdsacondition)
1212
- [JsonApiCondition](#jsonapicondition)
@@ -100,6 +100,20 @@ _Object containing the following properties:_
100100

101101
_(\*) Required._
102102

103+
## ContextVariableCondition
104+
105+
Context Variable Condition for performing comparison operations on context variable values.
106+
107+
_Object containing the following properties:_
108+
109+
| Property | Description | Type |
110+
| :------------------------- | :------------------------------------------------------------------------------------------- | :------------------------------------------------ |
111+
| **`conditionType`** (\*) | | `'context-variable'` |
112+
| **`contextVariable`** (\*) | The context variable to check (e.g., ":userAddress", ":customParam") | `string` (_regex: `/^:[a-zA-Z_][a-zA-Z0-9_]*$/`_) |
113+
| **`returnValueTest`** (\*) | Test to perform on a value. Supports comparison operators like ==, >, <, >=, <=, !=, in, !in | [ReturnValueTest](#returnvaluetest) |
114+
115+
_(\*) Required._
116+
103117
## ContractCondition
104118

105119
_Object containing the following properties:_
@@ -232,6 +246,8 @@ _(\*) Required._
232246

233247
## ReturnValueTest
234248

249+
Test to perform on a value. Supports comparison operators like ==, >, <, >=, <=, !=, in, !in
250+
235251
_Object containing the following properties:_
236252

237253
| Property | Type |
@@ -344,20 +360,6 @@ _Object containing the following properties:_
344360

345361
_(\*) Required._
346362

347-
## AddressAllowlistCondition
348-
349-
Address Allowlist Condition for allowing decryption for specific wallet addresses. It is very handy when combined with other conditions.
350-
351-
_Object containing the following properties:_
352-
353-
| Property | Description | Type |
354-
| :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- |
355-
| **`conditionType`** (\*) | | `'address-allowlist'` |
356-
| **`userAddress`** (\*) | This is a context variable that will be replaced at decryption time. It represents the Ethereum address of the requester attempting decryption. | [UserAddress](#useraddress) |
357-
| **`addresses`** (\*) | List of wallet addresses allowed to decrypt. Addresses should be provided in checksummed form. | `Array<string>` (_min: 1, max: 25_) |
358-
359-
_(\*) Required._
360-
361363
## More resources
362364

363365
For more information, please refer to the TACo documentation:

packages/taco/src/conditions/base/address-allowlist.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Condition } from '../condition';
2+
import {
3+
ContextVariableConditionProps,
4+
contextVariableConditionSchema,
5+
ContextVariableConditionType,
6+
} from '../schemas/context-variable';
7+
import { OmitConditionType } from '../shared';
8+
9+
export { ContextVariableConditionProps, ContextVariableConditionType };
10+
11+
/**
12+
* A condition that performs comparison operations on context variable values.
13+
*/
14+
export class ContextVariableCondition extends Condition {
15+
constructor(value: OmitConditionType<ContextVariableConditionProps>) {
16+
super(contextVariableConditionSchema, {
17+
conditionType: ContextVariableConditionType,
18+
...value,
19+
});
20+
}
21+
}

packages/taco/src/conditions/base/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Exporting classes here instead of their respective schema files to
22
// avoid circular dependency on Condition class.
33

4-
export * as addressAllowlist from './address-allowlist';
4+
export * as contextVariable from './context-variable';
55
export * as contract from './contract';
66
export * as ecdsa from './ecdsa';
77
export * as jsonApi from './json-api';

packages/taco/src/conditions/condition-factory.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {
2-
AddressAllowlistCondition,
3-
AddressAllowlistConditionProps,
4-
AddressAllowlistConditionType,
5-
} from './base/address-allowlist';
2+
ContextVariableCondition,
3+
ContextVariableConditionProps,
4+
ContextVariableConditionType,
5+
} from './base/context-variable';
66
import {
77
ContractCondition,
88
ContractConditionProps,
@@ -76,10 +76,6 @@ export class ConditionFactory {
7676
return new JsonRpcCondition(props as JsonRpcConditionProps);
7777
case JWTConditionType:
7878
return new JWTCondition(props as JWTConditionProps);
79-
case AddressAllowlistConditionType:
80-
return new AddressAllowlistCondition(
81-
props as AddressAllowlistConditionProps,
82-
);
8379
case SigningObjectAttributeConditionType:
8480
return new SigningObjectAttributeCondition(
8581
props as SigningObjectAttributeConditionProps,
@@ -88,6 +84,10 @@ export class ConditionFactory {
8884
return new SigningObjectAbiAttributeCondition(
8985
props as SigningObjectAbiAttributeConditionProps,
9086
);
87+
case ContextVariableConditionType:
88+
return new ContextVariableCondition(
89+
props as ContextVariableConditionProps,
90+
);
9191
// Logical Conditions
9292
case CompoundConditionType:
9393
return new CompoundCondition(props as CompoundConditionProps);

0 commit comments

Comments
 (0)