Skip to content
Open
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
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
]
},
"dependencies": {
"borsh": "^2.0.0",
"ky": "^1.12.0",
"viem": "~2.45.1"
},
Expand Down
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export const MULTICHAIN_INPUT_SETTLER_ESCROW =
"0xb912b4c38ab54b94D45Ac001484dEBcbb519Bc2B" as const;
export const MULTICHAIN_INPUT_SETTLER_COMPACT =
"0x1fccC0807F25A58eB531a0B5b4bf3dCE88808Ed7" as const;


// Solana config
export const SOLANA_INPUT_SETTLER_ESCROW =
"0x4186c46d62fb033aace3a262def7efbbef0591b8e98732bcd62edbbc0916da57" as const;

1 change: 1 addition & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type IntentDeps = {
verifier: CoreVerifier,
chainId: bigint,
) => `0x${string}` | undefined;
getSettler?: (chainId: bigint) => `0x${string}` | undefined;
};

export type StandardOrderValidationDeps = {
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export function toBigIntWithDecimals(value: number, decimals: number): bigint {
}

export function addressToBytes32(address: `0x${string}`): `0x${string}` {
if (address.length === 66) return address; // already bytes32 with 0x prefix
if (address.length === 64) return `0x${address}`; // already bytes32 without 0x
Copy link
Member

Choose a reason for hiding this comment

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

Does this properly catch 64 length addresses prefixed with 0x?

I would rather use something like: 0x${address.replace("0x", "").padStart("0", 64)}

Copy link
Author

@Asem-Abdelhady Asem-Abdelhady Mar 13, 2026

Choose a reason for hiding this comment

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

yeah i think we should remove if (address.length === 64) also we don't need to check the address length in address.length !== 40 for EVM since we are already constraining the template with address: 0x${string} in the parameters.

// Accept only EVM addresses here.
if (address.length !== 42 && address.length !== 40) {
throw new Error(`Invalid address length: ${address.length}`);
}
Expand Down
60 changes: 50 additions & 10 deletions src/intent/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,49 @@ import type {
CompactLock,
CreateIntentOptions,
EscrowLock,
SolanaEscrowLock,
MultichainOrder,
StandardOrder,
TokenContext,
SolanaStandardOrder,
} from "../types/index";
import { MultichainOrderIntent } from "./multichain";
import { StandardOrderIntent } from "./standard";
import { buildMandateOutputs } from "./helpers/output-encoding";
import { ONE_DAY, ONE_HOUR, inputSettlerForLock } from "./helpers/shared";
import { addressToBytes32 } from "../helpers/convert";
import { SolanaStandardOrderIntent } from "./solanaStandard";


/**
* @notice Class representing a Li.Fi Intent. Contains intent abstractions and helpers.
*/
export class Intent {
private lock: EscrowLock | CompactLock;
private lock: EscrowLock | SolanaEscrowLock | CompactLock;

private user: `0x${string}`;
private walletUser: `0x${string}`;
private inputs: TokenContext[];
private outputs: TokenContext[];
private getOracle: IntentDeps["getOracle"];
private getSettler: IntentDeps["getSettler"];
private verifier: string;
private exclusiveFor?: `0x${string}`;
private outputRecipient?: `0x${string}`;

private _nonce?: bigint;
private expiry = ONE_DAY;
private fillDeadline = 2 * ONE_HOUR;

constructor(opts: CreateIntentOptions, deps: IntentDeps) {
this.lock = opts.lock;
this.user = opts.account;
this.walletUser = opts.account;
this.inputs = opts.inputTokens;
this.outputs = opts.outputTokens;
this.verifier = opts.verifier;
this.getOracle = deps.getOracle;
this.getSettler = deps.getSettler;
this.exclusiveFor = opts.exclusiveFor;
this.outputRecipient = opts.outputRecipient;
}

numInputChains() {
Expand Down Expand Up @@ -98,12 +107,40 @@ export class Intent {
]);

const currentTime = Math.floor(Date.now() / 1000);
const bytes32Recipient = this.outputRecipient ? addressToBytes32(this.outputRecipient) : addressToBytes32(this.walletUser);
Copy link
Member

Choose a reason for hiding this comment

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

No need for bytes32Recipient. Call it recipient and add comments


if (this.lock.type === "solanaEscrow") {
Copy link
Member

Choose a reason for hiding this comment

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

See previous message. This does not seem correct.

const inputOracle = this.getOracle(this.verifier, inputChain)!;
const solanaStandardOrder: SolanaStandardOrder = {
user: this.walletUser,
nonce: this.nonce(),
originChainId: inputChain,
fillDeadline: currentTime + this.fillDeadline,
expires: currentTime + this.expiry,
inputOracle,
input: {
token: firstInput.token.address,
amount: firstInput.amount,
},
Comment on lines +121 to +124
Copy link
Member

Choose a reason for hiding this comment

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

Only 1 input? Please add verification for only 1.

Copy link
Author

Choose a reason for hiding this comment

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

Yes solana supports only one, at least for now. I agree there should be length verification

outputs: buildMandateOutputs({
exclusiveFor: this.exclusiveFor,
outputTokens: this.outputs,
getOracle: this.getOracle,
getSettler: this.getSettler,
verifier: this.verifier,
sameChain: this.isSameChain(),
bytes32Recipient,
currentTime,
}),
};
return new SolanaStandardOrderIntent(inputSettlerForLock(this.lock, false), solanaStandardOrder);
}
const inputOracle = this.isSameChain()
? COIN_FILLER
: this.getOracle(this.verifier, inputChain)!;

const order: StandardOrder = {
user: this.user,
user: this.walletUser,
nonce: this.nonce(),
originChainId: inputChain,
fillDeadline: currentTime + this.fillDeadline,
Expand All @@ -114,9 +151,10 @@ export class Intent {
exclusiveFor: this.exclusiveFor,
outputTokens: this.outputs,
getOracle: this.getOracle,
getSettler: this.getSettler,
verifier: this.verifier,
sameChain: this.isSameChain(),
recipient: this.user,
bytes32Recipient,
currentTime,
}),
};
Expand All @@ -137,7 +175,7 @@ export class Intent {
this.verifier,
firstInput.token.chainId,
)!;

const bytes32Recipient = this.outputRecipient ? addressToBytes32(this.outputRecipient) : addressToBytes32(this.walletUser);
const inputs: { chainId: bigint; inputs: [bigint, bigint][] }[] = [
...new Set(this.inputs.map(({ token }) => token.chainId)),
].map((chain) => {
Expand All @@ -160,8 +198,9 @@ export class Intent {
};
});


const order: MultichainOrder = {
user: this.user,
user: this.walletUser,
nonce: this.nonce(),
fillDeadline: currentTime + this.fillDeadline,
expires: currentTime + this.expiry,
Expand All @@ -170,9 +209,10 @@ export class Intent {
exclusiveFor: this.exclusiveFor,
outputTokens: this.outputs,
getOracle: this.getOracle,
getSettler: this.getSettler,
verifier: this.verifier,
sameChain: false,
recipient: this.user,
bytes32Recipient,
currentTime,
}),
inputs,
Expand All @@ -181,7 +221,7 @@ export class Intent {
return new MultichainOrderIntent(
inputSettlerForLock(this.lock, true),
order,
this.lock,
this.lock as EscrowLock | CompactLock,
);
}

Expand Down
40 changes: 28 additions & 12 deletions src/intent/fromOrder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
CompactLock,
EscrowLock,
MultichainOrder,
SolanaStandardOrder,
StandardOrder,
} from "../types/index";
import {
Expand All @@ -11,14 +12,20 @@ import {
import { ResetPeriod } from "../compact/idLib";
import { MultichainOrderIntent } from "./multichain";
import { StandardOrderIntent } from "./standard";
import { SolanaStandardOrderIntent } from "./solanaStandard";

type OrderLike = StandardOrder | MultichainOrder;
type OrderLike = StandardOrder | SolanaStandardOrder | MultichainOrder;

type StandardOrderToIntentOptions = {
inputSettler: `0x${string}`;
order: StandardOrder;
};

type SolanaStandardOrderToIntentOptions = {
inputSettler: `0x${string}`;
order: SolanaStandardOrder;
};

type MultichainOrderToIntentOptions = {
inputSettler: `0x${string}`;
order: MultichainOrder;
Expand Down Expand Up @@ -47,31 +54,40 @@ function inferLock(inputSettler: `0x${string}`): EscrowLock | CompactLock {
}

export function isStandardOrder(order: OrderLike): order is StandardOrder {
return "originChainId" in order;
return "originChainId" in order && "inputs" in order;
Copy link
Member

Choose a reason for hiding this comment

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

There is no way this works. Because multichain order has inputs.

Copy link
Author

@Asem-Abdelhady Asem-Abdelhady Mar 13, 2026

Choose a reason for hiding this comment

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

multichain does not have originChainId so there shouldn't be a problem with that

}

export function isSolanaStandardOrder(
order: OrderLike,
): order is SolanaStandardOrder {
return "originChainId" in order && "input" in order;
}

export function orderToIntent(
options: StandardOrderToIntentOptions,
): StandardOrderIntent;
export function orderToIntent(
options: SolanaStandardOrderToIntentOptions,
): SolanaStandardOrderIntent;
export function orderToIntent(
options: MultichainOrderToIntentOptions,
): MultichainOrderIntent;
export function orderToIntent(
options: OrderToIntentOptions,
): StandardOrderIntent | MultichainOrderIntent;
): StandardOrderIntent | SolanaStandardOrderIntent | MultichainOrderIntent;
export function orderToIntent(
options:
| StandardOrderToIntentOptions
| SolanaStandardOrderToIntentOptions
| MultichainOrderToIntentOptions
| OrderToIntentOptions,
): StandardOrderIntent | MultichainOrderIntent {
): StandardOrderIntent | SolanaStandardOrderIntent | MultichainOrderIntent {
const { inputSettler, order } = options;
if (isStandardOrder(order)) {
return new StandardOrderIntent(inputSettler, order);
}
return new MultichainOrderIntent(
inputSettler,
order,
(options as MultichainOrderToIntentOptions).lock ?? inferLock(inputSettler),
);

if (isStandardOrder(order)) return new StandardOrderIntent(inputSettler, order);
if (isSolanaStandardOrder(order)) return new SolanaStandardOrderIntent(inputSettler, order);

const lock = "lock" in options ? options.lock ?? inferLock(inputSettler) : inferLock(inputSettler);

return new MultichainOrderIntent(inputSettler, order, lock);
}
10 changes: 6 additions & 4 deletions src/intent/helpers/output-encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,20 @@ export function buildMandateOutputs(options: {
exclusiveFor?: `0x${string}`;
outputTokens: TokenContext[];
getOracle: IntentDeps["getOracle"];
getSettler?: IntentDeps["getSettler"];
verifier: CoreVerifier;
sameChain: boolean;
recipient: `0x${string}`;
bytes32Recipient: `0x${string}`;
currentTime: number;
}): MandateOutput[] {
const {
exclusiveFor,
outputTokens,
getOracle,
getSettler,
verifier,
sameChain,
recipient,
bytes32Recipient,
Copy link
Member

Choose a reason for hiding this comment

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

I don't like using bytes32Recipient since it seems inaccurate. I would rather continue using recipient and then add natspec that recipient has to be bytes32.

Copy link
Author

Choose a reason for hiding this comment

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

i agree

currentTime,
} = options;

Expand All @@ -50,8 +52,8 @@ export function buildMandateOutputs(options: {
);
}

const outputSettler = COIN_FILLER;
return outputTokens.map(({ token, amount }) => {
const outputSettler = getSettler?.(token.chainId) ?? COIN_FILLER;
Copy link
Member

Choose a reason for hiding this comment

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

Potentially bad fallback for Solana consumers

const outputOracle = sameChain
? addressToBytes32(outputSettler)
: addressToBytes32(getOracle(verifier, token.chainId)!);
Expand All @@ -61,7 +63,7 @@ export function buildMandateOutputs(options: {
chainId: token.chainId,
token: addressToBytes32(token.address),
amount: amount,
recipient: addressToBytes32(recipient),
recipient: bytes32Recipient,
callbackData: "0x",
context,
};
Expand Down
7 changes: 5 additions & 2 deletions src/intent/helpers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import {
INPUT_SETTLER_ESCROW_LIFI,
MULTICHAIN_INPUT_SETTLER_COMPACT,
MULTICHAIN_INPUT_SETTLER_ESCROW,
SOLANA_INPUT_SETTLER_ESCROW,
} from "../../constants";
import type { CompactLock, EscrowLock } from "../../types";
import type { CompactLock, EscrowLock, SolanaEscrowLock } from "../../types";

export const ONE_MINUTE = 60;
export const ONE_HOUR = 60 * ONE_MINUTE;
Expand All @@ -15,7 +16,7 @@ export function selectAllBut<T>(arr: T[], index: number): T[] {
}

export function inputSettlerForLock(
lock: EscrowLock | CompactLock,
lock: EscrowLock | CompactLock | SolanaEscrowLock,
multichain: boolean,
) {
if (lock.type === "compact" && multichain === false)
Expand All @@ -26,6 +27,8 @@ export function inputSettlerForLock(
return INPUT_SETTLER_ESCROW_LIFI;
if (lock.type === "escrow" && multichain === true)
return MULTICHAIN_INPUT_SETTLER_ESCROW;
if (lock.type === "solanaEscrow" && multichain === false)
return SOLANA_INPUT_SETTLER_ESCROW;
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

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

Why do we add a new lock type for Solana?

Why not add chain type or similar as the differentiator?

Copy link
Author

Choose a reason for hiding this comment

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

Since we're following the structure anyway i believe it's a good idea to commit to everything

Copy link
Author

Choose a reason for hiding this comment

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

what i'm also okay with is adding a field inside the lock that identifies the chain


throw new Error(
`Not supported | multichain: ${multichain}, type: ${lock.type}`,
Expand Down
7 changes: 6 additions & 1 deletion src/intent/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export { Intent } from "./create";
export { isStandardOrder, orderToIntent } from "./fromOrder";
export {
SolanaStandardOrderIntent,
computeSolanaStandardOrderId,
standardOrderToSolanaOrder,
} from "./solanaStandard";
export { isStandardOrder, isSolanaStandardOrder, orderToIntent } from "./fromOrder";
export { StandardOrderIntent, computeStandardOrderId } from "./standard";
export type { OrderIntentCommon } from "./types";
export {
Expand Down
Loading