Add configured withdrawal defaults
All checks were successful
deploy / deploy (push) Successful in 22s
All checks were successful
deploy / deploy (push) Successful in 22s
Proof: The funded NEAR Intents operator path should have a stable configured withdrawal destination for the active assets so exits do not depend on retyping recipient addresses. Assumptions: Active asset withdrawal destinations are long-lived operator settings and can safely live in runtime config; actual withdrawals still require explicit unfreeze and operator action. Still fake: Strategy and executor remain disarmed, no live trade quote has been submitted, and the live withdrawal transaction itself has not been exercised yet.
This commit is contained in:
parent
3f0a119987
commit
b4186d9715
7 changed files with 48 additions and 7 deletions
|
|
@ -14,10 +14,12 @@ TRADING_BTC_ASSET_ID=nep141:btc.omft.near
|
||||||
TRADING_BTC_SYMBOL=BTC
|
TRADING_BTC_SYMBOL=BTC
|
||||||
TRADING_BTC_DECIMALS=8
|
TRADING_BTC_DECIMALS=8
|
||||||
TRADING_BTC_CHAIN=btc:mainnet
|
TRADING_BTC_CHAIN=btc:mainnet
|
||||||
|
TRADING_BTC_WITHDRAW_ADDRESS=
|
||||||
TRADING_EURE_ASSET_ID=nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
TRADING_EURE_ASSET_ID=nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
||||||
TRADING_EURE_SYMBOL=EURe
|
TRADING_EURE_SYMBOL=EURe
|
||||||
TRADING_EURE_DECIMALS=18
|
TRADING_EURE_DECIMALS=18
|
||||||
TRADING_EURE_CHAIN=eth:100
|
TRADING_EURE_CHAIN=eth:100
|
||||||
|
TRADING_EURE_WITHDRAW_ADDRESS=
|
||||||
|
|
||||||
# Control APIs
|
# Control APIs
|
||||||
NEAR_INTENTS_CONTROL_API_ENABLED=true
|
NEAR_INTENTS_CONTROL_API_ENABLED=true
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ data:
|
||||||
TRADING_BTC_SYMBOL: BTC
|
TRADING_BTC_SYMBOL: BTC
|
||||||
TRADING_BTC_DECIMALS: "8"
|
TRADING_BTC_DECIMALS: "8"
|
||||||
TRADING_BTC_CHAIN: btc:mainnet
|
TRADING_BTC_CHAIN: btc:mainnet
|
||||||
|
TRADING_BTC_WITHDRAW_ADDRESS: ""
|
||||||
TRADING_EURE_ASSET_ID: nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
TRADING_EURE_ASSET_ID: nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near
|
||||||
TRADING_EURE_SYMBOL: EURe
|
TRADING_EURE_SYMBOL: EURe
|
||||||
TRADING_EURE_DECIMALS: "18"
|
TRADING_EURE_DECIMALS: "18"
|
||||||
TRADING_EURE_CHAIN: "eth:100"
|
TRADING_EURE_CHAIN: "eth:100"
|
||||||
|
TRADING_EURE_WITHDRAW_ADDRESS: "0x6C40267e03A97B2132e7a7d3159C88534eBEfdFb"
|
||||||
NEAR_INTENTS_CONTROL_API_ENABLED: "true"
|
NEAR_INTENTS_CONTROL_API_ENABLED: "true"
|
||||||
NEAR_INTENTS_CONTROL_HOST: 0.0.0.0
|
NEAR_INTENTS_CONTROL_HOST: 0.0.0.0
|
||||||
NEAR_INTENTS_CONTROL_PORT: "8081"
|
NEAR_INTENTS_CONTROL_PORT: "8081"
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ Notes:
|
||||||
|
|
||||||
- Deposit addresses are built in. `liquidity-manager` refreshes them from the bridge `deposit_address` RPC and exposes them through `/state`.
|
- Deposit addresses are built in. `liquidity-manager` refreshes them from the bridge `deposit_address` RPC and exposes them through `/state`.
|
||||||
- The repo withdrawal action is for external-chain exits on the active assets. It submits `intents.near::ft_withdraw`, using the active OMFT token contract as `receiver_id` and `memo=WITHDRAW_TO:<destination>`, then tracks the returned NEAR transaction hash through bridge `withdrawal_status`.
|
- The repo withdrawal action is for external-chain exits on the active assets. It submits `intents.near::ft_withdraw`, using the active OMFT token contract as `receiver_id` and `memo=WITHDRAW_TO:<destination>`, then tracks the returned NEAR transaction hash through bridge `withdrawal_status`.
|
||||||
|
- `destination_address` can be omitted only when a default withdrawal address is configured for that asset via `TRADING_BTC_WITHDRAW_ADDRESS` or `TRADING_EURE_WITHDRAW_ADDRESS`.
|
||||||
- Withdrawals stay frozen by default. Unfreeze explicitly before calling `/withdraw`, then freeze them again after the operation if you do not want further exits.
|
- Withdrawals stay frozen by default. Unfreeze explicitly before calling `/withdraw`, then freeze them again after the operation if you do not want further exits.
|
||||||
|
|
||||||
## Safe arming sequence
|
## Safe arming sequence
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,10 @@ const controlApi = startControlApi({
|
||||||
getState() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
account_id: config.nearIntentsAccountId,
|
account_id: config.nearIntentsAccountId,
|
||||||
|
withdrawal_defaults: {
|
||||||
|
[config.tradingBtc.assetId]: config.tradingBtc.withdrawAddress || null,
|
||||||
|
[config.tradingEure.assetId]: config.tradingEure.withdrawAddress || null,
|
||||||
|
},
|
||||||
...store.getState(),
|
...store.getState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -374,10 +378,10 @@ const controlApi = startControlApi({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/withdrawal-estimate',
|
path: '/withdrawal-estimate',
|
||||||
handler: async ({ body }) => {
|
handler: async ({ body }) => {
|
||||||
if (!body.asset_id || !body.amount || !body.destination_address) {
|
if (!body.asset_id || !body.amount) {
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
payload: { error: 'asset_id, amount, and destination_address are required' },
|
payload: { error: 'asset_id and amount are required' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -450,10 +454,10 @@ const controlApi = startControlApi({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: '/withdraw',
|
path: '/withdraw',
|
||||||
handler: async ({ body }) => {
|
handler: async ({ body }) => {
|
||||||
if (!body.asset_id || !body.amount || !body.destination_address) {
|
if (!body.asset_id || !body.amount) {
|
||||||
return {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
payload: { error: 'asset_id, amount, and destination_address are required' },
|
payload: { error: 'asset_id and amount are required' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,20 @@ export function buildBridgeWithdrawalPlan({
|
||||||
}) {
|
}) {
|
||||||
const normalizedAssetId = String(assetId || '').trim();
|
const normalizedAssetId = String(assetId || '').trim();
|
||||||
const normalizedAmount = String(amount || '').trim();
|
const normalizedAmount = String(amount || '').trim();
|
||||||
const normalizedDestination = String(destinationAddress || '').trim();
|
const requestedDestination = String(destinationAddress || '').trim();
|
||||||
const requestedChain = chain == null ? null : String(chain).trim();
|
const requestedChain = chain == null ? null : String(chain).trim();
|
||||||
|
|
||||||
if (!normalizedAssetId) throw new Error('asset_id is required');
|
if (!normalizedAssetId) throw new Error('asset_id is required');
|
||||||
if (!/^\d+$/.test(normalizedAmount) || BigInt(normalizedAmount) <= 0n) {
|
if (!/^\d+$/.test(normalizedAmount) || BigInt(normalizedAmount) <= 0n) {
|
||||||
throw new Error('amount must be a positive integer string in smallest units');
|
throw new Error('amount must be a positive integer string in smallest units');
|
||||||
}
|
}
|
||||||
if (!normalizedDestination) throw new Error('destination_address is required');
|
|
||||||
|
|
||||||
const asset = config.assetRegistry.get(normalizedAssetId);
|
const asset = config.assetRegistry.get(normalizedAssetId);
|
||||||
if (!asset) throw new Error(`unsupported asset_id: ${normalizedAssetId}`);
|
if (!asset) throw new Error(`unsupported asset_id: ${normalizedAssetId}`);
|
||||||
if (requestedChain && requestedChain !== asset.chain) {
|
if (requestedChain && requestedChain !== asset.chain) {
|
||||||
throw new Error(`chain mismatch for ${normalizedAssetId}: expected ${asset.chain}`);
|
throw new Error(`chain mismatch for ${normalizedAssetId}: expected ${asset.chain}`);
|
||||||
}
|
}
|
||||||
|
const normalizedDestination = requestedDestination || String(asset.withdrawAddress || '').trim();
|
||||||
|
|
||||||
const nearTokenId = stripAssetPrefix(normalizedAssetId);
|
const nearTokenId = stripAssetPrefix(normalizedAssetId);
|
||||||
const supported = Object.values(supportedTokens).find((token) => (
|
const supported = Object.values(supportedTokens).find((token) => (
|
||||||
|
|
@ -33,6 +33,10 @@ export function buildBridgeWithdrawalPlan({
|
||||||
throw new Error(`unsupported bridge withdrawal token: ${normalizedAssetId}`);
|
throw new Error(`unsupported bridge withdrawal token: ${normalizedAssetId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!normalizedDestination) {
|
||||||
|
throw new Error(`destination_address is required for ${normalizedAssetId}`);
|
||||||
|
}
|
||||||
|
|
||||||
const minWithdrawalAmount = String(supported.min_withdrawal_amount || '0');
|
const minWithdrawalAmount = String(supported.min_withdrawal_amount || '0');
|
||||||
if (BigInt(normalizedAmount) < BigInt(minWithdrawalAmount)) {
|
if (BigInt(normalizedAmount) < BigInt(minWithdrawalAmount)) {
|
||||||
throw new Error(`amount below minimum withdrawal: ${minWithdrawalAmount}`);
|
throw new Error(`amount below minimum withdrawal: ${minWithdrawalAmount}`);
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,12 @@ const DEFAULTS = {
|
||||||
tradingBtcSymbol: 'BTC',
|
tradingBtcSymbol: 'BTC',
|
||||||
tradingBtcDecimals: 8,
|
tradingBtcDecimals: 8,
|
||||||
tradingBtcChain: 'btc:mainnet',
|
tradingBtcChain: 'btc:mainnet',
|
||||||
|
tradingBtcWithdrawAddress: '',
|
||||||
tradingEureAssetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
tradingEureAssetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
||||||
tradingEureSymbol: 'EURe',
|
tradingEureSymbol: 'EURe',
|
||||||
tradingEureDecimals: 18,
|
tradingEureDecimals: 18,
|
||||||
tradingEureChain: 'eth:100',
|
tradingEureChain: 'eth:100',
|
||||||
|
tradingEureWithdrawAddress: '',
|
||||||
marketReferenceRefreshMs: 5_000,
|
marketReferenceRefreshMs: 5_000,
|
||||||
marketReferenceCoinGeckoRefreshMs: 15_000,
|
marketReferenceCoinGeckoRefreshMs: 15_000,
|
||||||
marketReferenceMaxAgeMs: 30_000,
|
marketReferenceMaxAgeMs: 30_000,
|
||||||
|
|
@ -84,12 +86,13 @@ function parseBoolean(value, fallback) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAsset({ assetId, symbol, decimals, chain }) {
|
function buildAsset({ assetId, symbol, decimals, chain, withdrawAddress = '' }) {
|
||||||
return {
|
return {
|
||||||
assetId,
|
assetId,
|
||||||
symbol,
|
symbol,
|
||||||
decimals,
|
decimals,
|
||||||
chain,
|
chain,
|
||||||
|
withdrawAddress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,12 +104,16 @@ export function loadConfig({ envPath = '.env' } = {}) {
|
||||||
symbol: process.env.TRADING_BTC_SYMBOL || DEFAULTS.tradingBtcSymbol,
|
symbol: process.env.TRADING_BTC_SYMBOL || DEFAULTS.tradingBtcSymbol,
|
||||||
decimals: parseNumber(process.env.TRADING_BTC_DECIMALS, DEFAULTS.tradingBtcDecimals),
|
decimals: parseNumber(process.env.TRADING_BTC_DECIMALS, DEFAULTS.tradingBtcDecimals),
|
||||||
chain: process.env.TRADING_BTC_CHAIN || DEFAULTS.tradingBtcChain,
|
chain: process.env.TRADING_BTC_CHAIN || DEFAULTS.tradingBtcChain,
|
||||||
|
withdrawAddress:
|
||||||
|
process.env.TRADING_BTC_WITHDRAW_ADDRESS || DEFAULTS.tradingBtcWithdrawAddress,
|
||||||
});
|
});
|
||||||
const tradingEure = buildAsset({
|
const tradingEure = buildAsset({
|
||||||
assetId: process.env.TRADING_EURE_ASSET_ID || DEFAULTS.tradingEureAssetId,
|
assetId: process.env.TRADING_EURE_ASSET_ID || DEFAULTS.tradingEureAssetId,
|
||||||
symbol: process.env.TRADING_EURE_SYMBOL || DEFAULTS.tradingEureSymbol,
|
symbol: process.env.TRADING_EURE_SYMBOL || DEFAULTS.tradingEureSymbol,
|
||||||
decimals: parseNumber(process.env.TRADING_EURE_DECIMALS, DEFAULTS.tradingEureDecimals),
|
decimals: parseNumber(process.env.TRADING_EURE_DECIMALS, DEFAULTS.tradingEureDecimals),
|
||||||
chain: process.env.TRADING_EURE_CHAIN || DEFAULTS.tradingEureChain,
|
chain: process.env.TRADING_EURE_CHAIN || DEFAULTS.tradingEureChain,
|
||||||
|
withdrawAddress:
|
||||||
|
process.env.TRADING_EURE_WITHDRAW_ADDRESS || DEFAULTS.tradingEureWithdrawAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectName = process.env.PROJECT_NAME || DEFAULTS.projectName;
|
const projectName = process.env.PROJECT_NAME || DEFAULTS.projectName;
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@ const config = {
|
||||||
['nep141:btc.omft.near', {
|
['nep141:btc.omft.near', {
|
||||||
assetId: 'nep141:btc.omft.near',
|
assetId: 'nep141:btc.omft.near',
|
||||||
chain: 'btc:mainnet',
|
chain: 'btc:mainnet',
|
||||||
|
withdrawAddress: '',
|
||||||
}],
|
}],
|
||||||
['nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near', {
|
['nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near', {
|
||||||
assetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
assetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
||||||
chain: 'eth:100',
|
chain: 'eth:100',
|
||||||
|
withdrawAddress: '0x6C40267e03A97B2132e7a7d3159C88534eBEfdFb',
|
||||||
}],
|
}],
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
@ -62,3 +64,22 @@ test('buildBridgeWithdrawalPlan rejects amounts below the bridge minimum', () =>
|
||||||
config,
|
config,
|
||||||
}), /amount below minimum withdrawal: 10000/);
|
}), /amount below minimum withdrawal: 10000/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('buildBridgeWithdrawalPlan falls back to configured asset withdrawal address', () => {
|
||||||
|
const plan = buildBridgeWithdrawalPlan({
|
||||||
|
assetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
||||||
|
amount: '1000000000000000000',
|
||||||
|
destinationAddress: '',
|
||||||
|
supportedTokens: {
|
||||||
|
eure: {
|
||||||
|
near_token_id: 'gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near',
|
||||||
|
defuse_asset_identifier: 'eth:100:0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430',
|
||||||
|
min_withdrawal_amount: '1',
|
||||||
|
withdrawal_fee: '200000000000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(plan.destination_address, '0x6C40267e03A97B2132e7a7d3159C88534eBEfdFb');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue