aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ark/manager.js12
-rw-r--r--src/ark/offboard.js14
-rw-r--r--src/features/ark.js11
-rw-r--r--src/features/messages.js16
-rw-r--r--src/i18n.js2
5 files changed, 45 insertions, 10 deletions
diff --git a/src/ark/manager.js b/src/ark/manager.js
index a190480..59f9f09 100644
--- a/src/ark/manager.js
+++ b/src/ark/manager.js
@@ -45,7 +45,7 @@ import {
encodeVtxoFromDecoded,
} from './refresh.js';
import {
- getOffboardFeeRate, feeRateKwu, offboardFee, offboardAttestation,
+ getOffboardFeeRate, feeRateKwu, offboardFeeParts, offboardAttestation,
prepareOffboard, validateOffboardTx, signOffboardForfeits, finishOffboard,
P2TR_DUST,
} from './offboard.js';
@@ -564,16 +564,18 @@ export class ArkManager {
const tip = await this.chain.tipHeight();
const satVkb = await getOffboardFeeRate(this.arkUrl);
const grossSat = inputs.reduce((n, v) => n + v.amountSat, 0);
- const feeSat = offboardFee({
+ const feeParts = offboardFeeParts({
spkLen: spk.length, satVkb, fees: this.info.offboardFees, tip,
inputs: inputs.map((v) => ({ amountSat: v.amountSat, expiryHeight: v.expiryHeight })),
});
+ const feeSat = feeParts.totalSat;
const netSat = grossSat - feeSat;
if (netSat < P2TR_DUST) throw new Error('ark balance too small to offboard after fees');
const action = {
id: `offboard-${Date.now()}`, type: 'offboard', step: 'created',
inputIds: inputs.map((v) => v.id), address, spkHex: hex.encode(spk),
- grossSat, feeSat, netSat, rateKwu: feeRateKwu(satVkb),
+ grossSat, feeSat, chainFeeSat: feeParts.chainSat, serviceFeeSat: feeParts.serviceSat,
+ netSat, rateKwu: feeRateKwu(satVkb),
};
for (const v of inputs) v.state = 'pending';
this.state.actions.push(action);
@@ -626,6 +628,8 @@ export class ArkManager {
this._movement({
type: 'offboard', amountSat: action.netSat, status: 'complete',
txid: action.txid, detail: `fee ${action.feeSat} sat`, to: action.address,
+ feeSat: action.feeSat, chainFeeSat: action.chainFeeSat, serviceFeeSat: action.serviceFeeSat,
+ inputIds: action.inputIds,
});
this._save();
} else {
@@ -640,6 +644,8 @@ export class ArkManager {
this._movement({
type: 'offboard', amountSat: action.netSat, status: 'complete',
txid: action.txid, detail: `fee ${action.feeSat} sat`, to: action.address,
+ feeSat: action.feeSat, chainFeeSat: action.chainFeeSat, serviceFeeSat: action.serviceFeeSat,
+ inputIds: action.inputIds,
});
this._save();
}
diff --git a/src/ark/offboard.js b/src/ark/offboard.js
index ae6530d..28fd190 100644
--- a/src/ark/offboard.js
+++ b/src/ark/offboard.js
@@ -60,9 +60,13 @@ export const feeRateKwu = (satVkb) => Math.floor(satVkb / 4);
// OffboardFees::calculate — base + ceil(rate * weight) + per-vtxo ppm-by-expiry.
// inputs: [{ amountSat, expiryHeight }]; fees: info.offboardFees.
-export function offboardFee({ spkLen, satVkb, fees, tip, inputs }) {
+// The fee splits into what the MINERS take (this offboard's weight in the
+// shared tx, at the current rate) and what the ASP charges for the service
+// (base fee + the per-coin lifetime ppm, same bracket table as renewals) —
+// kept separate so the wallet can show them apart.
+export function offboardFeeParts({ spkLen, satVkb, fees, tip, inputs }) {
const wu = (fees.fixedAdditionalVb + spkLen) * 4;
- const weightFee = Math.floor((feeRateKwu(satVkb) * wu + 999) / 1000);
+ const chainSat = Math.floor((feeRateKwu(satVkb) * wu + 999) / 1000);
// pver >= 4: sub-satoshi ppm accumulates across vtxos, one round-up at the end
let ppmUnits = 0;
for (const v of inputs) {
@@ -70,7 +74,11 @@ export function offboardFee({ spkLen, satVkb, fees, tip, inputs }) {
const entry = [...fees.ppmExpiryTable].reverse().find((e) => blocks >= e.thresholdBlocks);
if (entry) ppmUnits += v.amountSat * entry.ppm;
}
- return fees.baseFeeSat + weightFee + Math.ceil(ppmUnits / 1_000_000);
+ const serviceSat = fees.baseFeeSat + Math.ceil(ppmUnits / 1_000_000);
+ return { chainSat, serviceSat, totalSat: chainSat + serviceSat };
+}
+export function offboardFee(args) {
+ return offboardFeeParts(args).totalSat;
}
// OffboardRequestAttestation: BIP340 signature over
diff --git a/src/features/ark.js b/src/features/ark.js
index c8e979b..fc5fc9f 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -205,6 +205,8 @@ export function mergeArkStates(a, b) {
// a fee learned late (the rescue computed it from inputs − outputs)
// upgrades a recorded zero — the row only renders when the fee is real
if ((rm.feeSat || 0) > 0 && !(lm.feeSat > 0)) take.feeSat = rm.feeSat;
+ if (rm.chainFeeSat != null && lm.chainFeeSat == null) take.chainFeeSat = rm.chainFeeSat;
+ if (rm.serviceFeeSat != null && lm.serviceFeeSat == null) take.serviceFeeSat = rm.serviceFeeSat;
return Object.keys(take).length ? { ...lm, ...take } : lm;
});
// Two devices (or the push and the poll, pre-guard) can each record the
@@ -1115,7 +1117,14 @@ export function arkFeature(ctx) {
})(),
m.to ? row(t('arkPayTo'), shortAddr(m.to, 16, 12)) : null,
m.vtxoId ? row(t('arkVtxoId'), shortTxid(m.vtxoId)) : null,
- m.detail ? row(t('detailsLabel'), m.detail) : null,
+ // An offboard's fee is two different pockets — the miners' cut for the
+ // on-chain weight and the ASP's service charge (base + lifetime ppm) —
+ // shown apart so neither reads as the other being overpriced.
+ m.chainFeeSat != null && m.serviceFeeSat != null
+ ? h('div', { class: 'col', style: 'gap:10px' },
+ row(t('arkFeeChain'), fmtAmount(m.chainFeeSat) + ' ' + unitLabel()),
+ row(t('arkFeeService'), fmtAmount(m.serviceFeeSat) + ' ' + unitLabel()))
+ : m.detail ? row(t('detailsLabel'), m.detail) : null,
// proof of payment: the preimage is what a merchant asks for
m.preimage ? row(t('preimageLabel'), shortTxid(m.preimage)) : null,
m.preimage ? h('button', { class: 'btn-block', onClick: () => ctx.copy(m.preimage) }, t('copyPreimage')) : null,
diff --git a/src/features/messages.js b/src/features/messages.js
index ac43e40..803afc9 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -2029,7 +2029,13 @@ export function messagesFeature(ctx) {
} }, t('msgDmsTitle')),
h('button', { class: 'grow', onClick: () => {
const npubStr = npubOf(pk);
+ // leave the whole messaging surface, not just the profile —
+ // a lingering search or thread would win the screen router
+ // and the Send tab would never appear
ui.profilePk = null;
+ ui.profOverThread = false;
+ ui.noteThread = null;
+ ui.userSearch = null;
ui.chatOpen = false;
ui.tab = 'send';
render();
@@ -2418,10 +2424,14 @@ export function messagesFeature(ctx) {
kids.push(h('div', { class: 'list' }, communities().map((jm) => {
const room = rooms.get(jm.community_id);
const name = room?.folded?.metadata?.name || jm.name;
- // until the guestbook fold lands, last session's settled count beats
- // a placeholder that flips a moment later
+ // Last session's settled count anchors the number: while the guestbook
+ // replay is still streaming in (multiple relays, bursts past the fold
+ // debounce), the live tally climbs through intermediate values — take
+ // the max so the count doesn't visibly tick upward on every refresh.
+ // Once the replay settles, the cache is rewritten to the live figure,
+ // so a genuine departure still shows (one quiet step down, not a climb).
const live = room ? [...room.members.values()].filter((m) => m.state === 'join').length : 0;
- const memberCount = live || (st().memberCounts || {})[jm.community_id] || 0;
+ const memberCount = Math.max(live, (st().memberCounts || {})[jm.community_id] || 0);
const unread = room ? roomUnread(room) : false;
return h('div', {
class: 'item chat-thread-row' + (unread ? ' unread' : ''),
diff --git a/src/i18n.js b/src/i18n.js
index d6d5740..0dbd06e 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -223,6 +223,8 @@ const STR = {
arkCoinsChipExpTitle: 'Time until this coin needs renewal',
arkCoinsChipDepthTitle: 'How many off-chain hops this coin carries',
arkCoinsChipExitTitle: 'What pulling this coin on-chain by itself would cost',
+ arkFeeChain: 'On-chain fee',
+ arkFeeService: 'Service fee',
arkCoinsExpired: 'expired',
arkCoinsExpiryNote: 'Past its expiry a coin can be reclaimed by the server — the wallet renews on its own well before that.',
arkCoinsExitTitle: 'Uncooperative exit',