Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@changesets/cli": "^2.28.1",
"@nucypher/nucypher-core": "^0.14.5",
"@nucypher/nucypher-core": "^0.15.1-dev.1",
"ethers": "^5.8.0"
},
"devDependencies": {
Expand Down Expand Up @@ -65,7 +65,7 @@
]
},
"overrides": {
"@nucypher/nucypher-core": "^0.14.5",
"@nucypher/nucypher-core": "^0.15.1-dev.1",
"glob-parent@<5.1.2": ">=5.1.2",
"node-forge@<1.0.0": ">=1.0.0",
"node-forge@<1.3.0": ">=1.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/pre/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"typedoc": "typedoc"
},
"dependencies": {
"@nucypher/nucypher-core": "^0.14.5",
"@nucypher/nucypher-core": "^0.15.1-dev.1",
"@nucypher/shared": "workspace:*",
"ethers": "^5.8.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"@ethersproject/abi": "^5.8.0",
"@ethersproject/providers": "^5.8.0",
"@nucypher/nucypher-contracts": "0.26.0-alpha.1",
"@nucypher/nucypher-core": "^0.14.5",
"@nucypher/nucypher-core": "^0.15.1-dev.1",
"axios": "^1.8.4",
"deep-equal": "^2.2.3",
"ethers": "^5.8.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Domain } from '../../porter';
import { SigningCoordinator__factory } from '../ethers-typechain';
import { SigningCoordinator } from '../ethers-typechain/SigningCoordinator';

type SignerInfo = {
export type SignerInfo = {
operator: string;
provider: string;
signature: string;
Expand Down
85 changes: 39 additions & 46 deletions packages/shared/src/porter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import {
CapsuleFrag,
EncryptedThresholdDecryptionRequest,
EncryptedThresholdDecryptionResponse,
PackedUserOperationSignatureRequest,
PublicKey,
RetrievalKit,
SignatureResponse,
TreasureMap,
UserOperationSignatureRequest,
} from '@nucypher/nucypher-core';
import axios, {
AxiosRequestConfig,
Expand Down Expand Up @@ -148,18 +151,24 @@ type PostTacoDecryptResponse = {
};

export type TacoDecryptResult = {
encryptedResponses: Record<string, EncryptedThresholdDecryptionResponse>;
errors: Record<string, string>;
encryptedResponses: Record<
ChecksumAddress,
EncryptedThresholdDecryptionResponse
>;
errors: Record<ChecksumAddress, string>;
};

// Signing types

type PostTacoSignRequest = {
readonly signing_requests: Record<ChecksumAddress, Base64EncodedBytes>;
readonly threshold: number;
};

type TacoSignResponse = {
readonly result: {
readonly signing_results: {
readonly signatures: Record<
ChecksumAddress,
[ChecksumAddress, Base64EncodedBytes]
>;
readonly signatures: Record<ChecksumAddress, Base64EncodedBytes>;
readonly errors: Record<ChecksumAddress, string>;
};
};
Expand All @@ -172,32 +181,10 @@ export type TacoSignature = {
};

export type TacoSignResult = {
signingResults: { [ursulaAddress: string]: TacoSignature };
errors: Record<string, string>;
signingResults: Record<ChecksumAddress, TacoSignature>;
errors: Record<ChecksumAddress, string>;
};

function decodeSignature(
signerAddress: string,
signatureB64: string,
): { result?: TacoSignature; error?: string } {
try {
const decodedData = JSON.parse(
new TextDecoder().decode(fromBase64(signatureB64)),
);
return {
result: {
messageHash: decodedData.message_hash,
signature: decodedData.signature,
signerAddress,
},
};
} catch (error) {
return {
error: `Failed to decode signature: ${error}`,
};
}
}

export class PorterClient {
readonly porterUrls: URL[];

Expand Down Expand Up @@ -345,40 +332,46 @@ export class PorterClient {
}

public async signUserOp(
signingRequests: Record<string, string>,
signingRequests: Record<
string,
UserOperationSignatureRequest | PackedUserOperationSignatureRequest
>,
threshold: number,
): Promise<TacoSignResult> {
const data: Record<string, unknown> = {
signing_requests: signingRequests,
threshold: threshold,
const data: PostTacoSignRequest = {
signing_requests: Object.fromEntries(
Object.entries(signingRequests).map(([ursula, signingRequest]) => [
ursula,
toBase64(signingRequest.toBytes()),
]),
),
threshold,
};

const resp: AxiosResponse<TacoSignResponse> = await this.tryAndCall({
url: '/sign',
method: 'post',
data,
});

const { signatures, errors } = resp.data.result.signing_results;
const allErrors: Record<string, string> = { ...errors };

const signingResults: { [ursulaAddress: string]: TacoSignature } = {};
for (const [ursulaAddress, [signerAddress, signatureB64]] of Object.entries(
for (const [ursulaAddress, signatureResponseBase64] of Object.entries(
signatures || {},
)) {
const decoded = decodeSignature(signerAddress, signatureB64);
if (decoded.error) {
// issue with decoding signature, add to errors
allErrors[ursulaAddress] = decoded.error;
continue;
}
// Always include all decoded signatures in signingResults
signingResults[ursulaAddress] = decoded.result!;
const signatureResponse = SignatureResponse.fromBytes(
fromBase64(signatureResponseBase64 as string),
);
signingResults[ursulaAddress] = {
messageHash: `0x${toHexString(signatureResponse.hash)}`,
signature: `0x${toHexString(signatureResponse.signature)}`,
signerAddress: signatureResponse.signer,
};
}

return {
signingResults,
errors: allErrors,
errors,
};
}
}
165 changes: 100 additions & 65 deletions packages/shared/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,117 @@
import { ethers } from 'ethers';
import { PackedUserOperation, UserOperation } from '@nucypher/nucypher-core';

import { fromHexString } from './utils';

export type ChecksumAddress = `0x${string}`;
export type HexEncodedBytes = string;
export type Base64EncodedBytes = string;

export type UserOperation = {
sender: string;
nonce: number;
factory: string;
factoryData: string;
callData: string;
callGasLimit: number;
verificationGasLimit: number;
preVerificationGas: number;
maxFeePerGas: number;
maxPriorityFeePerGas: number;
paymaster: string;
paymasterVerificationGasLimit: number;
paymasterPostOpGasLimit: number;
paymasterData: string;
signature: string;
export type UserOperationToSign = {
sender: `0x${string}`;
nonce: bigint | number;
callData: `0x${string}` | Uint8Array;
callGasLimit: bigint | number;
verificationGasLimit: bigint | number;
preVerificationGas: bigint | number;
maxFeePerGas: bigint | number;
maxPriorityFeePerGas: bigint | number;
// optional fields
factory?: `0x${string}` | undefined;
factoryData?: `0x${string}` | Uint8Array | undefined;
paymaster?: `0x${string}` | undefined;
paymasterVerificationGasLimit?: bigint | number | undefined;
paymasterPostOpGasLimit?: bigint | number | undefined;
paymasterData?: `0x${string}` | Uint8Array | undefined;
signature?: `0x${string}` | Uint8Array | undefined;
};

export class UserOperationSignatureRequest {
constructor(
private userOp: ReturnType<typeof convertUserOperationToPython>,
private aaVersion: string,
private cohortId: number,
private chainId: number,
private context: unknown = {},
private signatureType: string = 'userop',
) {}
export type PackedUserOperationToSign = {
sender: `0x${string}`;
nonce: bigint | number;
initCode: `0x${string}` | Uint8Array;
callData: `0x${string}` | Uint8Array;
accountGasLimit: `0x${string}` | Uint8Array;
preVerificationGas: bigint | number;
gasFees: `0x${string}` | Uint8Array;
paymasterAndData: `0x${string}` | Uint8Array;
};

toBytes(): Uint8Array {
const data = {
user_op: JSON.stringify(this.userOp),
aa_version: this.aaVersion,
cohort_id: this.cohortId,
chain_id: this.chainId,
context: this.context,
signature_type: this.signatureType,
};
return new TextEncoder().encode(JSON.stringify(data));
}
function getBigIntValue(value: bigint | number): bigint {
return typeof value === 'bigint' ? value : BigInt(value);
}

function normalizeAddress(address: string): string | null {
if (!address || address === '0x') {
return null;
function getUint8ArrayValue(value: `0x${string}` | Uint8Array): Uint8Array {
return value instanceof Uint8Array ? value : fromHexString(value);
}

export function toCoreUserOperation(
userOperation: UserOperationToSign,
): UserOperation {
const userOp = new UserOperation(
userOperation.sender,
getBigIntValue(userOperation.nonce),
getUint8ArrayValue(userOperation.callData),
getBigIntValue(userOperation.callGasLimit),
getBigIntValue(userOperation.verificationGasLimit),
getBigIntValue(userOperation.preVerificationGas),
getBigIntValue(userOperation.maxFeePerGas),
getBigIntValue(userOperation.maxPriorityFeePerGas),
);

// optional factory data
if (userOperation.factory) {
let factory_data = undefined;
if (userOperation.factoryData) {
factory_data = getUint8ArrayValue(userOperation.factoryData);
}

userOp.setFactoryData(userOperation.factory, factory_data);
}

try {
// Use ethers to get the checksummed address - this will throw on invalid addresses
return ethers.utils.getAddress(address);
} catch (error) {
// Re-throw the error to fail fast on invalid addresses
throw new Error(
`Invalid address: ${address}. ${error instanceof Error ? error.message : 'Unknown error'}`,
// optional paymaster data
if (userOperation.paymaster) {
if (!userOperation.paymasterVerificationGasLimit) {
throw new Error(
'paymasterVerificationGasLimit is required when paymaster is set',
);
} else if (!userOperation.paymasterPostOpGasLimit) {
throw new Error(
'paymasterPostOpGasLimit is required when paymaster is set',
);
}

userOp.setPaymasterData(
userOperation.paymaster,
getBigIntValue(userOperation.paymasterVerificationGasLimit),
getBigIntValue(userOperation.paymasterPostOpGasLimit),
userOperation.paymasterData
? getUint8ArrayValue(userOperation.paymasterData)
: undefined,
);
}

return userOp;
}

export function toCorePackedUserOperation(
packedUserOperation: PackedUserOperationToSign,
): PackedUserOperation {
const packedUserOp = new PackedUserOperation(
packedUserOperation.sender,
getBigIntValue(packedUserOperation.nonce),
getUint8ArrayValue(packedUserOperation.initCode),
getUint8ArrayValue(packedUserOperation.callData),
getUint8ArrayValue(packedUserOperation.accountGasLimit),
getBigIntValue(packedUserOperation.preVerificationGas),
getUint8ArrayValue(packedUserOperation.gasFees),
getUint8ArrayValue(packedUserOperation.paymasterAndData),
);

return packedUserOp;
}

export function convertUserOperationToPython(userOp: UserOperation) {
return {
sender: normalizeAddress(userOp.sender),
nonce: userOp.nonce,
factory: normalizeAddress(userOp.factory),
factory_data: userOp.factoryData || '0x',
call_data: userOp.callData || '0x',
call_gas_limit: userOp.callGasLimit,
verification_gas_limit: userOp.verificationGasLimit,
pre_verification_gas: userOp.preVerificationGas,
max_fee_per_gas: userOp.maxFeePerGas,
max_priority_fee_per_gas: userOp.maxPriorityFeePerGas,
paymaster: normalizeAddress(userOp.paymaster),
paymaster_verification_gas_limit: userOp.paymasterVerificationGasLimit,
paymaster_post_op_gas_limit: userOp.paymasterPostOpGasLimit,
paymaster_data: userOp.paymasterData || '0x',
signature: userOp.signature || '0x',
};
export function isPackedUserOperation(
op: UserOperationToSign | PackedUserOperationToSign,
): op is PackedUserOperationToSign {
return 'initCode' in op && 'gasFees' in op;
}
Loading
Loading