Merge branch 'master' into age-withdraw

This commit is contained in:
Özgür Kesim 2023-08-03 19:35:52 +02:00
commit 77ea209ddb
Signed by: oec
GPG Key ID: 3D76A56D79EDD9D7
51 changed files with 580 additions and 449 deletions

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util"; import { Amounts, HttpStatusCode, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { import {
ErrorType, ErrorType,
HttpResponsePaginated, HttpResponsePaginated,
@ -27,6 +27,7 @@ import { useAccountDetails } from "../hooks/access.js";
import { LoginForm } from "./LoginForm.js"; import { LoginForm } from "./LoginForm.js";
import { PaymentOptions } from "./PaymentOptions.js"; import { PaymentOptions } from "./PaymentOptions.js";
import { notifyError } from "../hooks/notification.js"; import { notifyError } from "../hooks/notification.js";
import { useEffect, useState } from "preact/hooks";
interface Props { interface Props {
account: string; account: string;
@ -34,6 +35,60 @@ interface Props {
error: HttpResponsePaginated<T, SandboxBackend.SandboxError>, error: HttpResponsePaginated<T, SandboxBackend.SandboxError>,
) => VNode; ) => VNode;
} }
export const CopyIcon = (): VNode => (
<svg height="16" viewBox="0 0 16 16" width="16">
<path
fill-rule="evenodd"
d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
/>
<path
fill-rule="evenodd"
d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
/>
</svg>
);
export const CopiedIcon = (): VNode => (
<svg height="16" viewBox="0 0 16 16" width="16">
<path
fill-rule="evenodd"
d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
/>
</svg>
);
function CopyButton({ getContent }: { getContent: () => string }): VNode {
const [copied, setCopied] = useState(false);
function copyText(): void {
navigator.clipboard.writeText(getContent() || "");
setCopied(true);
}
useEffect(() => {
if (copied) {
setTimeout(() => {
setCopied(false);
}, 1000);
}
}, [copied]);
if (!copied) {
return (
<button onClick={copyText} style={{width:32, height:32, fontSize: "initial"}}>
<CopyIcon />
</button>
);
}
return (
<div content="Copied" style={{display:"inline-block"}}>
<button disabled style={{width:32, height:32 , fontSize: "initial"}}>
<CopiedIcon />
</button>
</div>
);
}
/** /**
* Query account information and show QR code if there is pending withdrawal * Query account information and show QR code if there is pending withdrawal
*/ */
@ -66,7 +121,6 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div> <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
); );
} }
const accountNumber = payto.iban;
const balanceIsDebit = data.balance.credit_debit_indicator == "debit"; const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
const limit = balanceIsDebit const limit = balanceIsDebit
? Amounts.sub(debitThreshold, balance).amount ? Amounts.sub(debitThreshold, balance).amount
@ -76,8 +130,7 @@ export function AccountPage({ account, onLoadNotOk }: Props): VNode {
<div> <div>
<h1 class="nav welcome-text"> <h1 class="nav welcome-text">
<i18n.Translate> <i18n.Translate>
Welcome, {accountNumber ? `${account} (${accountNumber})` : account} Welcome, {account} (<a href={stringifyPaytoUri(payto)}>{payto.iban}</a>)! <CopyButton getContent={() => stringifyPaytoUri(payto)} />
!
</i18n.Translate> </i18n.Translate>
</h1> </h1>
</div> </div>

View File

@ -44,7 +44,7 @@ export function InputSelector<T>({
fromStr = defaultFromString, fromStr = defaultFromString,
toStr = defaultToString, toStr = defaultToString,
}: Props<keyof T>): VNode { }: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name); const { error, value, onChange, required } = useField<T>(name);
return ( return (
<div class="field is-horizontal"> <div class="field is-horizontal">
<div class="field-label is-normal"> <div class="field-label is-normal">
@ -58,8 +58,8 @@ export function InputSelector<T>({
</label> </label>
</div> </div>
<div class="field-body is-flex-grow-3"> <div class="field-body is-flex-grow-3">
<div class="field"> <div class="field has-icons-right">
<p class={expand ? "control is-expanded select" : "control select"}> <p class={expand ? "control is-expanded select" : "control select "}>
<select <select
class={error ? "select is-danger" : "select"} class={error ? "select is-danger" : "select"}
name={String(name)} name={String(name)}
@ -78,8 +78,14 @@ export function InputSelector<T>({
); );
})} })}
</select> </select>
{help} {help}
</p> </p>
{required && (
<span class="icon has-text-danger is-right" style={{height: "2.5em"}}>
<i class="mdi mdi-alert" />
</span>
)}
{error && <p class="help is-danger">{error}</p>} {error && <p class="help is-danger">{error}</p>}
</div> </div>
</div> </div>

View File

@ -1331,12 +1331,13 @@ export namespace MerchantBackend {
} }
namespace Webhooks { namespace Webhooks {
type MerchantWebhookType = "pay" | "refund";
interface WebhookAddDetails { interface WebhookAddDetails {
// Webhook ID to use. // Webhook ID to use.
webhook_id: string; webhook_id: string;
// The event of the webhook: why the webhook is used. // The event of the webhook: why the webhook is used.
event_type: string; event_type: MerchantWebhookType;
// URL of the webhook where the customer will be redirected. // URL of the webhook where the customer will be redirected.
url: string; url: string;

View File

@ -33,6 +33,7 @@ import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputNumber } from "../../../../components/form/InputNumber.js";
import { useBackendContext } from "../../../../context/backend.js"; import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js"; import { MerchantBackend } from "../../../../declaration.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
type Entity = MerchantBackend.Webhooks.WebhookAddDetails; type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
@ -50,7 +51,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const errors: FormErrors<Entity> = { const errors: FormErrors<Entity> = {
webhook_id: !state.webhook_id ? i18n.str`required` : undefined, webhook_id: !state.webhook_id ? i18n.str`required` : undefined,
event_type: !state.event_type ? i18n.str`required` : undefined, event_type: !state.event_type ? i18n.str`required`
: state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"`
: undefined,
http_method: !state.http_method http_method: !state.http_method
? i18n.str`required` ? i18n.str`required`
: !validMethod.includes(state.http_method) : !validMethod.includes(state.http_method)
@ -84,16 +87,30 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
label={i18n.str`ID`} label={i18n.str`ID`}
tooltip={i18n.str`Webhook ID to use`} tooltip={i18n.str`Webhook ID to use`}
/> />
<Input<Entity> <InputSelector
name="event_type" name="event_type"
label={i18n.str`Event`} label={i18n.str`Event`}
values={[
i18n.str`Choose one...`,
i18n.str`pay`,
i18n.str`refund`,
]}
tooltip={i18n.str`The event of the webhook: why the webhook is used`} tooltip={i18n.str`The event of the webhook: why the webhook is used`}
/> />
<Input<Entity> <InputSelector
name="http_method" name="http_method"
label={i18n.str`Method`} label={i18n.str`Method`}
values={[
i18n.str`Choose one...`,
i18n.str`GET`,
i18n.str`POST`,
i18n.str`PUT`,
i18n.str`PATCH`,
i18n.str`HEAD`,
]}
tooltip={i18n.str`Method used by the webhook`} tooltip={i18n.str`Method used by the webhook`}
/> />
<Input<Entity> <Input<Entity>
name="url" name="url"
label={i18n.str`URL`} label={i18n.str`URL`}

View File

@ -51,8 +51,8 @@ import {
stringToBytes, stringToBytes,
TalerError, TalerError,
TalerProtocolDuration, TalerProtocolDuration,
TipCreateConfirmation, RewardCreateConfirmation,
TipCreateRequest, RewardCreateRequest,
TippingReserveStatus, TippingReserveStatus,
WalletNotification, WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -1751,8 +1751,8 @@ export namespace MerchantPrivateApi {
export async function giveTip( export async function giveTip(
merchantService: MerchantServiceInterface, merchantService: MerchantServiceInterface,
instance: string, instance: string,
req: TipCreateRequest, req: RewardCreateRequest,
): Promise<TipCreateConfirmation> { ): Promise<RewardCreateConfirmation> {
const reqUrl = new URL( const reqUrl = new URL(
`private/tips`, `private/tips`,
merchantService.makeInstanceBaseUrl(instance), merchantService.makeInstanceBaseUrl(instance),

View File

@ -191,12 +191,12 @@ export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
const walletTipping = new WalletCli(t, "age-tipping"); const walletTipping = new WalletCli(t, "age-tipping");
const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, { const ptr = await walletTipping.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: tip.taler_tip_uri, talerRewardUri: tip.taler_reward_uri,
}); });
await walletTipping.client.call(WalletApiOperation.AcceptTip, { await walletTipping.client.call(WalletApiOperation.AcceptReward, {
walletTipId: ptr.walletTipId, walletRewardId: ptr.walletRewardId,
}); });
await walletTipping.runUntilDone(); await walletTipping.runUntilDone();

View File

@ -17,13 +17,20 @@
/** /**
* Imports. * Imports.
*/ */
import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; import {
AbsoluteTime,
Duration,
NotificationType,
TransactionMajorState,
TransactionMinorState,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js"; import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { GlobalTestState, WalletCli } from "../harness/harness.js";
import { import {
createSimpleTestkudosEnvironment, createSimpleTestkudosEnvironmentV2,
withdrawViaBank, createWalletDaemonWithClient,
withdrawViaBankV2,
} from "../harness/helpers.js"; } from "../harness/helpers.js";
/** /**
@ -32,12 +39,7 @@ import {
export async function runAgeRestrictionsPeerTest(t: GlobalTestState) { export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(
wallet: walletOne,
bank,
exchange,
merchant,
} = await createSimpleTestkudosEnvironment(
t, t,
defaultCoinConfig.map((x) => x("TESTKUDOS")), defaultCoinConfig.map((x) => x("TESTKUDOS")),
{ {
@ -45,20 +47,29 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
}, },
); );
const walletTwo = new WalletCli(t, "walletTwo"); const w1 = await createWalletDaemonWithClient(t, {
const walletThree = new WalletCli(t, "walletThree"); name: "w1",
persistent: true,
});
const w2 = await createWalletDaemonWithClient(t, {
name: "w2",
persistent: true,
});
const wallet1 = w1.walletClient;
const wallet2 = w2.walletClient;
{ {
const wallet = walletOne; const withdrawalRes = await withdrawViaBankV2(t, {
walletClient: wallet1,
await withdrawViaBank(t, {
wallet,
bank, bank,
exchange, exchange,
amount: "TESTKUDOS:20", amount: "TESTKUDOS:20",
restrictAge: 13, restrictAge: 13,
}); });
await withdrawalRes.withdrawalFinishedCond;
const purse_expiration = AbsoluteTime.toProtocolTimestamp( const purse_expiration = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration( AbsoluteTime.addDuration(
AbsoluteTime.now(), AbsoluteTime.now(),
@ -66,7 +77,7 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
), ),
); );
const initResp = await wallet.client.call( const initResp = await wallet1.client.call(
WalletApiOperation.InitiatePeerPushDebit, WalletApiOperation.InitiatePeerPushDebit,
{ {
partialContractTerms: { partialContractTerms: {
@ -77,20 +88,35 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
}, },
); );
await wallet.runUntilDone(); const peerPushReadyCond = wallet1.waitForNotificationCond(
(x) =>
x.type === NotificationType.TransactionStateTransition &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready &&
x.transactionId === initResp.transactionId,
);
const checkResp = await walletTwo.client.call( await peerPushReadyCond;
const checkResp = await wallet2.call(
WalletApiOperation.PreparePeerPushCredit, WalletApiOperation.PreparePeerPushCredit,
{ {
talerUri: initResp.talerUri, talerUri: initResp.talerUri,
}, },
); );
await walletTwo.client.call(WalletApiOperation.ConfirmPeerPushCredit, { await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, {
peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId, peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
}); });
await walletTwo.runUntilDone(); const peerPullCreditDoneCond = wallet2.waitForNotificationCond(
(x) =>
x.type === NotificationType.TransactionStateTransition &&
x.newTxState.major === TransactionMajorState.Done &&
x.transactionId === checkResp.transactionId,
);
await peerPullCreditDoneCond;
} }
} }

View File

@ -23,6 +23,7 @@ import {
j2s, j2s,
NotificationType, NotificationType,
TransactionMajorState, TransactionMajorState,
TransactionMinorState,
WalletNotification, WalletNotification,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@ -46,12 +47,14 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
const w1 = await createWalletDaemonWithClient(t, { const w1 = await createWalletDaemonWithClient(t, {
name: "w1", name: "w1",
persistent: true,
handleNotification(wn) { handleNotification(wn) {
allW1Notifications.push(wn); allW1Notifications.push(wn);
}, },
}); });
const w2 = await createWalletDaemonWithClient(t, { const w2 = await createWalletDaemonWithClient(t, {
name: "w2", name: "w2",
persistent: true,
handleNotification(wn) { handleNotification(wn) {
allW2Notifications.push(wn); allW2Notifications.push(wn);
}, },
@ -89,6 +92,15 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
}, },
); );
const peerPullCreditReadyCond = wallet1.waitForNotificationCond(
(x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId &&
x.newTxState.major === TransactionMajorState.Pending &&
x.newTxState.minor === TransactionMinorState.Ready,
);
await peerPullCreditReadyCond;
const checkResp = await wallet2.client.call( const checkResp = await wallet2.client.call(
WalletApiOperation.PreparePeerPullDebit, WalletApiOperation.PreparePeerPullDebit,
{ {
@ -98,8 +110,6 @@ export async function runPeerToPeerPullTest(t: GlobalTestState) {
console.log(`checkResp: ${j2s(checkResp)}`); console.log(`checkResp: ${j2s(checkResp)}`);
// FIXME: The wallet should emit a more appropriate notification here.
// Yes, it's technically a withdrawal.
const peerPullCreditDoneCond = wallet1.waitForNotificationCond( const peerPullCreditDoneCond = wallet1.waitForNotificationCond(
(x) => x.type === NotificationType.TransactionStateTransition && (x) => x.type === NotificationType.TransactionStateTransition &&
x.transactionId === resp.transactionId && x.transactionId === resp.transactionId &&

View File

@ -99,17 +99,17 @@ export async function runTippingTest(t: GlobalTestState) {
console.log("created tip", tip); console.log("created tip", tip);
const doTip = async (): Promise<void> => { const doTip = async (): Promise<void> => {
const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, { const ptr = await wallet.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: tip.taler_tip_uri, talerRewardUri: tip.taler_reward_uri,
}); });
console.log(ptr); console.log(ptr);
t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); t.assertAmountEquals(ptr.rewardAmountRaw, "TESTKUDOS:5");
t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); t.assertAmountEquals(ptr.rewardAmountEffective, "TESTKUDOS:4.85");
await wallet.client.call(WalletApiOperation.AcceptTip, { await wallet.client.call(WalletApiOperation.AcceptReward, {
walletTipId: ptr.walletTipId, walletRewardId: ptr.walletRewardId,
}); });
await wallet.runUntilDone(); await wallet.runUntilDone();
@ -127,7 +127,7 @@ export async function runTippingTest(t: GlobalTestState) {
console.log("Transactions:", JSON.stringify(txns, undefined, 2)); console.log("Transactions:", JSON.stringify(txns, undefined, 2));
t.assertDeepEqual(txns.transactions[0].type, "tip"); t.assertDeepEqual(txns.transactions[0].type, "reward");
t.assertDeepEqual(txns.transactions[0].txState.major, TransactionMajorState.Done); t.assertDeepEqual(txns.transactions[0].txState.major, TransactionMajorState.Done);
t.assertAmountEquals( t.assertAmountEquals(
txns.transactions[0].amountEffective, txns.transactions[0].amountEffective,

View File

@ -499,7 +499,7 @@ export interface BackupRecoupGroup {
export enum BackupCoinSourceType { export enum BackupCoinSourceType {
Withdraw = "withdraw", Withdraw = "withdraw",
Refresh = "refresh", Refresh = "refresh",
Tip = "tip", Reward = "reward",
} }
/** /**
@ -546,7 +546,7 @@ export interface BackupRefreshCoinSource {
* Metadata about a coin obtained from a tip. * Metadata about a coin obtained from a tip.
*/ */
export interface BackupTipCoinSource { export interface BackupTipCoinSource {
type: BackupCoinSourceType.Tip; type: BackupCoinSourceType.Reward;
/** /**
* Wallet's identifier for the tip that this coin * Wallet's identifier for the tip that this coin

View File

@ -183,7 +183,16 @@ export class HttpLibImpl implements HttpRequestLibrary {
resolve(resp); resolve(resp);
}); });
res.on("error", (e) => { res.on("error", (e) => {
reject(e); const err = TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: url,
requestMethod: method,
httpStatusCode: 0,
},
`Error in HTTP response handler: ${e.message}`,
);
reject(err);
}); });
}; };
@ -197,7 +206,16 @@ export class HttpLibImpl implements HttpRequestLibrary {
} }
req.on("error", (e: Error) => { req.on("error", (e: Error) => {
reject(e); const err = TalerError.fromDetail(
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
{
requestUrl: url,
requestMethod: method,
httpStatusCode: 0,
},
`Error in HTTP request: ${e.message}`,
);
reject(err);
}); });
if (reqBody) { if (reqBody) {

View File

@ -290,22 +290,22 @@ export interface ReserveStatusEntry {
active: boolean; active: boolean;
} }
export interface TipCreateConfirmation { export interface RewardCreateConfirmation {
// Unique tip identifier for the tip that was created. // Unique tip identifier for the tip that was created.
tip_id: string; reward_id: string;
// taler://tip URI for the tip // taler://tip URI for the tip
taler_tip_uri: string; taler_reward_uri: string;
// URL that will directly trigger processing // URL that will directly trigger processing
// the tip when the browser is redirected to it // the tip when the browser is redirected to it
tip_status_url: string; reward_status_url: string;
// when does the tip expire // when does the reward expire
tip_expiration: AbsoluteTime; reward_expiration: AbsoluteTime;
} }
export interface TipCreateRequest { export interface RewardCreateRequest {
// Amount that the customer should be tipped // Amount that the customer should be tipped
amount: AmountString; amount: AmountString;

View File

@ -21,7 +21,7 @@ import {
parsePayUri, parsePayUri,
parseRefundUri, parseRefundUri,
parseRestoreUri, parseRestoreUri,
parseTipUri, parseRewardUri,
parseWithdrawExchangeUri, parseWithdrawExchangeUri,
parseWithdrawUri, parseWithdrawUri,
stringifyPayPushUri, stringifyPayPushUri,
@ -161,7 +161,7 @@ test("taler refund uri parsing with instance", (t) => {
test("taler tip pickup uri", (t) => { test("taler tip pickup uri", (t) => {
const url1 = "taler://tip/merchant.example.com/tipid"; const url1 = "taler://tip/merchant.example.com/tipid";
const r1 = parseTipUri(url1); const r1 = parseRewardUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -171,7 +171,7 @@ test("taler tip pickup uri", (t) => {
test("taler tip pickup uri with instance", (t) => { test("taler tip pickup uri with instance", (t) => {
const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid"; const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
const r1 = parseTipUri(url1); const r1 = parseRewardUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -182,7 +182,7 @@ test("taler tip pickup uri with instance", (t) => {
test("taler tip pickup uri with instance and prefix", (t) => { test("taler tip pickup uri with instance and prefix", (t) => {
const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid"; const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
const r1 = parseTipUri(url1); const r1 = parseRewardUri(url1);
if (!r1) { if (!r1) {
t.fail(); t.fail();
return; return;
@ -367,6 +367,6 @@ test("taler withdraw exchange URI with amount (stringify)", (t) => {
}); });
t.deepEqual( t.deepEqual(
url, url,
"taler://withdraw-exchange/exchange.demo.taler.net/JFX1NE38C65A5XT8VSNQXX7R7BBG4GNZ63F5T7Y6859V4J8KBKF0?a=KUDOS%3A19", "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A19",
); );
}); });

View File

@ -26,7 +26,7 @@ export type TalerUri =
| PayPushUriResult | PayPushUriResult
| BackupRestoreUri | BackupRestoreUri
| RefundUriResult | RefundUriResult
| TipUriResult | RewardUriResult
| WithdrawUriResult | WithdrawUriResult
| ExchangeUri | ExchangeUri
| WithdrawExchangeUri | WithdrawExchangeUri
@ -60,8 +60,8 @@ export interface RefundUriResult {
orderId: string; orderId: string;
} }
export interface TipUriResult { export interface RewardUriResult {
type: TalerUriAction.Tip; type: TalerUriAction.Reward;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;
} }
@ -167,7 +167,7 @@ export enum TalerUriAction {
Pay = "pay", Pay = "pay",
Withdraw = "withdraw", Withdraw = "withdraw",
Refund = "refund", Refund = "refund",
Tip = "tip", Reward = "reward",
PayPull = "pay-pull", PayPull = "pay-pull",
PayPush = "pay-push", PayPush = "pay-push",
PayTemplate = "pay-template", PayTemplate = "pay-template",
@ -212,7 +212,7 @@ const parsers: { [A in TalerUriAction]: Parser } = {
[TalerUriAction.PayTemplate]: parsePayTemplateUri, [TalerUriAction.PayTemplate]: parsePayTemplateUri,
[TalerUriAction.Restore]: parseRestoreUri, [TalerUriAction.Restore]: parseRestoreUri,
[TalerUriAction.Refund]: parseRefundUri, [TalerUriAction.Refund]: parseRefundUri,
[TalerUriAction.Tip]: parseTipUri, [TalerUriAction.Reward]: parseRewardUri,
[TalerUriAction.Withdraw]: parseWithdrawUri, [TalerUriAction.Withdraw]: parseWithdrawUri,
[TalerUriAction.DevExperiment]: parseDevExperimentUri, [TalerUriAction.DevExperiment]: parseDevExperimentUri,
[TalerUriAction.Exchange]: parseExchangeUri, [TalerUriAction.Exchange]: parseExchangeUri,
@ -255,8 +255,8 @@ export function stringifyTalerUri(uri: TalerUri): string {
case TalerUriAction.Refund: { case TalerUriAction.Refund: {
return stringifyRefundUri(uri); return stringifyRefundUri(uri);
} }
case TalerUriAction.Tip: { case TalerUriAction.Reward: {
return stringifyTipUri(uri); return stringifyRewardUri(uri);
} }
case TalerUriAction.Withdraw: { case TalerUriAction.Withdraw: {
return stringifyWithdrawUri(uri); return stringifyWithdrawUri(uri);
@ -394,11 +394,11 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
} }
/** /**
* Parse a taler[+http]://tip URI. * Parse a taler[+http]://reward URI.
* Return undefined if not passed a valid URI. * Return undefined if not passed a valid URI.
*/ */
export function parseTipUri(s: string): TipUriResult | undefined { export function parseRewardUri(s: string): RewardUriResult | undefined {
const pi = parseProtoInfo(s, "tip"); const pi = parseProtoInfo(s, "reward");
if (!pi) { if (!pi) {
return undefined; return undefined;
} }
@ -416,7 +416,7 @@ export function parseTipUri(s: string): TipUriResult | undefined {
); );
return { return {
type: TalerUriAction.Tip, type: TalerUriAction.Reward,
merchantBaseUrl, merchantBaseUrl,
merchantTipId: tipId, merchantTipId: tipId,
}; };
@ -699,12 +699,12 @@ export function stringifyRefundUri({
const { proto, path } = getUrlInfo(merchantBaseUrl); const { proto, path } = getUrlInfo(merchantBaseUrl);
return `${proto}://refund/${path}${orderId}`; return `${proto}://refund/${path}${orderId}`;
} }
export function stringifyTipUri({ export function stringifyRewardUri({
merchantBaseUrl, merchantBaseUrl,
merchantTipId, merchantTipId,
}: Omit<TipUriResult, "type">): string { }: Omit<RewardUriResult, "type">): string {
const { proto, path } = getUrlInfo(merchantBaseUrl); const { proto, path } = getUrlInfo(merchantBaseUrl);
return `${proto}://tip/${path}${merchantTipId}`; return `${proto}://reward/${path}${merchantTipId}`;
} }
export function stringifyExchangeUri({ export function stringifyExchangeUri({

View File

@ -186,7 +186,7 @@ export type Transaction =
| TransactionWithdrawal | TransactionWithdrawal
| TransactionPayment | TransactionPayment
| TransactionRefund | TransactionRefund
| TransactionTip | TransactionReward
| TransactionRefresh | TransactionRefresh
| TransactionDeposit | TransactionDeposit
| TransactionPeerPullCredit | TransactionPeerPullCredit
@ -201,7 +201,7 @@ export enum TransactionType {
Payment = "payment", Payment = "payment",
Refund = "refund", Refund = "refund",
Refresh = "refresh", Refresh = "refresh",
Tip = "tip", Reward = "reward",
Deposit = "deposit", Deposit = "deposit",
PeerPushDebit = "peer-push-debit", PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit", PeerPushCredit = "peer-push-credit",
@ -591,8 +591,8 @@ export interface TransactionRefund extends TransactionCommon {
paymentInfo: RefundPaymentInfo | undefined; paymentInfo: RefundPaymentInfo | undefined;
} }
export interface TransactionTip extends TransactionCommon { export interface TransactionReward extends TransactionCommon {
type: TransactionType.Tip; type: TransactionType.Reward;
// Raw amount of the tip, without extra fees that apply // Raw amount of the tip, without extra fees that apply
amountRaw: AmountString; amountRaw: AmountString;

View File

@ -605,7 +605,7 @@ export interface PrepareTipResult {
* *
* @deprecated use transactionId instead * @deprecated use transactionId instead
*/ */
walletTipId: string; walletRewardId: string;
/** /**
* Tip transaction ID. * Tip transaction ID.
@ -620,13 +620,13 @@ export interface PrepareTipResult {
/** /**
* Amount that the merchant gave. * Amount that the merchant gave.
*/ */
tipAmountRaw: AmountString; rewardAmountRaw: AmountString;
/** /**
* Amount that arrived at the wallet. * Amount that arrived at the wallet.
* Might be lower than the raw amount due to fees. * Might be lower than the raw amount due to fees.
*/ */
tipAmountEffective: AmountString; rewardAmountEffective: AmountString;
/** /**
* Base URL of the merchant backend giving then tip. * Base URL of the merchant backend giving then tip.
@ -654,14 +654,14 @@ export interface AcceptTipResponse {
export const codecForPrepareTipResult = (): Codec<PrepareTipResult> => export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
buildCodecForObject<PrepareTipResult>() buildCodecForObject<PrepareTipResult>()
.property("accepted", codecForBoolean()) .property("accepted", codecForBoolean())
.property("tipAmountRaw", codecForAmountString()) .property("rewardAmountRaw", codecForAmountString())
.property("tipAmountEffective", codecForAmountString()) .property("rewardAmountEffective", codecForAmountString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("merchantBaseUrl", codecForString()) .property("merchantBaseUrl", codecForString())
.property("expirationTimestamp", codecForTimestamp) .property("expirationTimestamp", codecForTimestamp)
.property("walletTipId", codecForString()) .property("walletRewardId", codecForString())
.property("transactionId", codecForString()) .property("transactionId", codecForString())
.build("PrepareTipResult"); .build("PrepareRewardResult");
export interface BenchmarkResult { export interface BenchmarkResult {
time: { [s: string]: number }; time: { [s: string]: number };
@ -994,6 +994,9 @@ export interface ExchangeDetailedResponse {
} }
export interface WalletCoreVersion { export interface WalletCoreVersion {
/**
* @deprecated
*/
hash: string | undefined; hash: string | undefined;
version: string; version: string;
exchange: string; exchange: string;
@ -1930,23 +1933,23 @@ export const codecForStartRefundQueryRequest =
.property("transactionId", codecForTransactionIdStr()) .property("transactionId", codecForTransactionIdStr())
.build("StartRefundQueryRequest"); .build("StartRefundQueryRequest");
export interface PrepareTipRequest { export interface PrepareRewardRequest {
talerTipUri: string; talerRewardUri: string;
} }
export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> => export const codecForPrepareRewardRequest = (): Codec<PrepareRewardRequest> =>
buildCodecForObject<PrepareTipRequest>() buildCodecForObject<PrepareRewardRequest>()
.property("talerTipUri", codecForString()) .property("talerRewardUri", codecForString())
.build("PrepareTipRequest"); .build("PrepareRewardRequest");
export interface AcceptTipRequest { export interface AcceptRewardRequest {
walletTipId: string; walletRewardId: string;
} }
export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> => export const codecForAcceptTipRequest = (): Codec<AcceptRewardRequest> =>
buildCodecForObject<AcceptTipRequest>() buildCodecForObject<AcceptRewardRequest>()
.property("walletTipId", codecForString()) .property("walletRewardId", codecForString())
.build("AcceptTipRequest"); .build("AcceptRewardRequest");
export interface FailTransactionRequest { export interface FailTransactionRequest {
transactionId: TransactionIdStr; transactionId: TransactionIdStr;

View File

@ -651,13 +651,13 @@ walletCli
alwaysYes: args.handleUri.autoYes, alwaysYes: args.handleUri.autoYes,
}); });
break; break;
case TalerUriAction.Tip: { case TalerUriAction.Reward: {
const res = await wallet.client.call(WalletApiOperation.PrepareTip, { const res = await wallet.client.call(WalletApiOperation.PrepareReward, {
talerTipUri: uri, talerRewardUri: uri,
}); });
console.log("tip status", res); console.log("tip status", res);
await wallet.client.call(WalletApiOperation.AcceptTip, { await wallet.client.call(WalletApiOperation.AcceptReward, {
walletTipId: res.walletTipId, walletRewardId: res.walletRewardId,
}); });
break; break;
} }

View File

@ -1,2 +1,3 @@
/lib /lib
/coverage /coverage
/src/version.json

View File

@ -28,7 +28,6 @@ import {
AgeCommitmentProof, AgeCommitmentProof,
AgeRestriction, AgeRestriction,
AmountJson, AmountJson,
AmountLike,
Amounts, Amounts,
AmountString, AmountString,
amountToBuffer, amountToBuffer,
@ -64,7 +63,6 @@ import {
hashCoinPub, hashCoinPub,
hashDenomPub, hashDenomPub,
hashTruncate32, hashTruncate32,
j2s,
kdf, kdf,
kdfKw, kdfKw,
keyExchangeEcdhEddsa, keyExchangeEcdhEddsa,
@ -81,16 +79,13 @@ import {
rsaVerify, rsaVerify,
setupTipPlanchet, setupTipPlanchet,
stringToBytes, stringToBytes,
TalerProtocolDuration,
TalerProtocolTimestamp, TalerProtocolTimestamp,
TalerSignaturePurpose, TalerSignaturePurpose,
timestampRoundedToBuffer, timestampRoundedToBuffer,
UnblindedSignature, UnblindedSignature,
validateIban,
WireFee, WireFee,
WithdrawalPlanchet, WithdrawalPlanchet,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import bigint from "big-integer";
// FIXME: Crypto should not use DB Types! // FIXME: Crypto should not use DB Types!
import { DenominationRecord } from "../db.js"; import { DenominationRecord } from "../db.js";
import { import {
@ -108,7 +103,6 @@ import {
EncryptContractForDepositResponse, EncryptContractForDepositResponse,
EncryptContractRequest, EncryptContractRequest,
EncryptContractResponse, EncryptContractResponse,
EncryptedContract,
SignDeletePurseRequest, SignDeletePurseRequest,
SignDeletePurseResponse, SignDeletePurseResponse,
SignPurseMergeRequest, SignPurseMergeRequest,

View File

@ -677,7 +677,7 @@ export interface PlanchetRecord {
export enum CoinSourceType { export enum CoinSourceType {
Withdraw = "withdraw", Withdraw = "withdraw",
Refresh = "refresh", Refresh = "refresh",
Tip = "tip", Reward = "reward",
} }
export interface WithdrawCoinSource { export interface WithdrawCoinSource {
@ -705,13 +705,13 @@ export interface RefreshCoinSource {
oldCoinPub: string; oldCoinPub: string;
} }
export interface TipCoinSource { export interface RewardCoinSource {
type: CoinSourceType.Tip; type: CoinSourceType.Reward;
walletTipId: string; walletRewardId: string;
coinIndex: number; coinIndex: number;
} }
export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource; export type CoinSource = WithdrawCoinSource | RefreshCoinSource | RewardCoinSource;
/** /**
* CoinRecord as stored in the "coins" data store * CoinRecord as stored in the "coins" data store
@ -815,9 +815,9 @@ export interface CoinAllocation {
} }
/** /**
* Status of a tip we got from a merchant. * Status of a reward we got from a merchant.
*/ */
export interface TipRecord { export interface RewardRecord {
/** /**
* Has the user accepted the tip? Only after the tip has been accepted coins * Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used. * withdrawn from the tip may be used.
@ -827,17 +827,17 @@ export interface TipRecord {
/** /**
* The tipped amount. * The tipped amount.
*/ */
tipAmountRaw: AmountString; rewardAmountRaw: AmountString;
/** /**
* Effect on the balance (including fees etc). * Effect on the balance (including fees etc).
*/ */
tipAmountEffective: AmountString; rewardAmountEffective: AmountString;
/** /**
* Timestamp, the tip can't be picked up anymore after this deadline. * Timestamp, the tip can't be picked up anymore after this deadline.
*/ */
tipExpiration: TalerProtocolTimestamp; rewardExpiration: TalerProtocolTimestamp;
/** /**
* The exchange that will sign our coins, chosen by the merchant. * The exchange that will sign our coins, chosen by the merchant.
@ -863,7 +863,7 @@ export interface TipRecord {
/** /**
* Tip ID chosen by the wallet. * Tip ID chosen by the wallet.
*/ */
walletTipId: string; walletRewardId: string;
/** /**
* Secret seed used to derive planchets for this tip. * Secret seed used to derive planchets for this tip.
@ -871,9 +871,9 @@ export interface TipRecord {
secretSeed: string; secretSeed: string;
/** /**
* The merchant's identifier for this tip. * The merchant's identifier for this reward.
*/ */
merchantTipId: string; merchantRewardId: string;
createdTimestamp: TalerPreciseTimestamp; createdTimestamp: TalerPreciseTimestamp;
@ -888,10 +888,10 @@ export interface TipRecord {
*/ */
pickedUpTimestamp: TalerPreciseTimestamp | undefined; pickedUpTimestamp: TalerPreciseTimestamp | undefined;
status: TipRecordStatus; status: RewardRecordStatus;
} }
export enum TipRecordStatus { export enum RewardRecordStatus {
PendingPickup = 10, PendingPickup = 10,
SuspendidPickup = 20, SuspendidPickup = 20,
@ -1420,7 +1420,7 @@ export interface KycPendingInfo {
} }
/** /**
* Group of withdrawal operations that need to be executed. * Group of withdrawal operations that need to be executed.
* (Either for a normal withdrawal or from a tip.) * (Either for a normal withdrawal or from a reward.)
* *
* The withdrawal group record is only created after we know * The withdrawal group record is only created after we know
* the coin selection we want to withdraw. * the coin selection we want to withdraw.
@ -2480,12 +2480,12 @@ export const WalletStoresV1 = {
]), ]),
}, },
), ),
tips: describeStore( rewards: describeStore(
"tips", "rewards",
describeContents<TipRecord>({ keyPath: "walletTipId" }), describeContents<RewardRecord>({ keyPath: "walletRewardId" }),
{ {
byMerchantTipIdAndBaseUrl: describeIndex("byMerchantTipIdAndBaseUrl", [ byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [
"merchantTipId", "merchantRewardId",
"merchantBaseUrl", "merchantBaseUrl",
]), ]),
byStatus: describeIndex("byStatus", "status", { byStatus: describeIndex("byStatus", "status", {
@ -2935,22 +2935,6 @@ export const walletDbFixups: FixupDescription[] = [
}); });
}, },
}, },
{
name: "TipRecordRecord_status_add",
async fn(tx): Promise<void> {
await tx.tips.iter().forEachAsync(async (r) => {
// Remove legacy transactions that don't have the totalCost field yet.
if (r.status == null) {
if (r.pickedUpTimestamp) {
r.status = TipRecordStatus.Done;
} else {
r.status = TipRecordStatus.PendingPickup;
}
await tx.tips.put(r);
}
});
},
},
{ {
name: "CoinAvailabilityRecord_visibleCoinCount_add", name: "CoinAvailabilityRecord_visibleCoinCount_add",
async fn(tx): Promise<void> { async fn(tx): Promise<void> {

View File

@ -96,7 +96,7 @@ export async function exportBackup(
x.purchases, x.purchases,
x.refreshGroups, x.refreshGroups,
x.backupProviders, x.backupProviders,
x.tips, x.rewards,
x.recoupGroups, x.recoupGroups,
x.withdrawalGroups, x.withdrawalGroups,
]) ])
@ -184,12 +184,12 @@ export async function exportBackup(
}); });
}); });
await tx.tips.iter().forEach((tip) => { await tx.rewards.iter().forEach((tip) => {
backupTips.push({ backupTips.push({
exchange_base_url: tip.exchangeBaseUrl, exchange_base_url: tip.exchangeBaseUrl,
merchant_base_url: tip.merchantBaseUrl, merchant_base_url: tip.merchantBaseUrl,
merchant_tip_id: tip.merchantTipId, merchant_tip_id: tip.merchantRewardId,
wallet_tip_id: tip.walletTipId, wallet_tip_id: tip.walletRewardId,
next_url: tip.next_url, next_url: tip.next_url,
secret_seed: tip.secretSeed, secret_seed: tip.secretSeed,
selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({ selected_denoms: tip.denomsSel.selectedDenoms.map((x) => ({
@ -199,8 +199,8 @@ export async function exportBackup(
timestamp_finished: tip.pickedUpTimestamp, timestamp_finished: tip.pickedUpTimestamp,
timestamp_accepted: tip.acceptedTimestamp, timestamp_accepted: tip.acceptedTimestamp,
timestamp_created: tip.createdTimestamp, timestamp_created: tip.createdTimestamp,
timestamp_expiration: tip.tipExpiration, timestamp_expiration: tip.rewardExpiration,
tip_amount_raw: Amounts.stringify(tip.tipAmountRaw), tip_amount_raw: Amounts.stringify(tip.rewardAmountRaw),
selected_denoms_uid: tip.denomSelUid, selected_denoms_uid: tip.denomSelUid,
}); });
}); });
@ -244,11 +244,11 @@ export async function exportBackup(
refresh_group_id: coin.coinSource.refreshGroupId, refresh_group_id: coin.coinSource.refreshGroupId,
}; };
break; break;
case CoinSourceType.Tip: case CoinSourceType.Reward:
bcs = { bcs = {
type: BackupCoinSourceType.Tip, type: BackupCoinSourceType.Reward,
coin_index: coin.coinSource.coinIndex, coin_index: coin.coinSource.coinIndex,
wallet_tip_id: coin.coinSource.walletTipId, wallet_tip_id: coin.coinSource.walletRewardId,
}; };
break; break;
case CoinSourceType.Withdraw: case CoinSourceType.Withdraw:

View File

@ -56,7 +56,7 @@ import {
WithdrawalGroupStatus, WithdrawalGroupStatus,
WithdrawalRecordType, WithdrawalRecordType,
RefreshOperationStatus, RefreshOperationStatus,
TipRecordStatus, RewardRecordStatus,
} from "../../db.js"; } from "../../db.js";
import { InternalWalletState } from "../../internal-wallet-state.js"; import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js"; import { assertUnreachable } from "../../util/assertUnreachable.js";
@ -250,11 +250,11 @@ export async function importCoin(
refreshGroupId: backupCoin.coin_source.refresh_group_id, refreshGroupId: backupCoin.coin_source.refresh_group_id,
}; };
break; break;
case BackupCoinSourceType.Tip: case BackupCoinSourceType.Reward:
coinSource = { coinSource = {
type: CoinSourceType.Tip, type: CoinSourceType.Reward,
coinIndex: backupCoin.coin_source.coin_index, coinIndex: backupCoin.coin_source.coin_index,
walletTipId: backupCoin.coin_source.wallet_tip_id, walletRewardId: backupCoin.coin_source.wallet_tip_id,
}; };
break; break;
case BackupCoinSourceType.Withdraw: case BackupCoinSourceType.Withdraw:
@ -311,7 +311,7 @@ export async function importBackup(
x.purchases, x.purchases,
x.refreshGroups, x.refreshGroups,
x.backupProviders, x.backupProviders,
x.tips, x.rewards,
x.recoupGroups, x.recoupGroups,
x.withdrawalGroups, x.withdrawalGroups,
x.tombstones, x.tombstones,
@ -812,13 +812,13 @@ export async function importBackup(
for (const backupTip of backupBlob.tips) { for (const backupTip of backupBlob.tips) {
const ts = constructTombstone({ const ts = constructTombstone({
tag: TombstoneTag.DeleteTip, tag: TombstoneTag.DeleteReward,
walletTipId: backupTip.wallet_tip_id, walletTipId: backupTip.wallet_tip_id,
}); });
if (tombstoneSet.has(ts)) { if (tombstoneSet.has(ts)) {
continue; continue;
} }
const existingTip = await tx.tips.get(backupTip.wallet_tip_id); const existingTip = await tx.rewards.get(backupTip.wallet_tip_id);
if (!existingTip) { if (!existingTip) {
const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw); const tipAmountRaw = Amounts.parseOrThrow(backupTip.tip_amount_raw);
const denomsSel = await getDenomSelStateFromBackup( const denomsSel = await getDenomSelStateFromBackup(
@ -827,22 +827,22 @@ export async function importBackup(
backupTip.exchange_base_url, backupTip.exchange_base_url,
backupTip.selected_denoms, backupTip.selected_denoms,
); );
await tx.tips.put({ await tx.rewards.put({
acceptedTimestamp: backupTip.timestamp_accepted, acceptedTimestamp: backupTip.timestamp_accepted,
createdTimestamp: backupTip.timestamp_created, createdTimestamp: backupTip.timestamp_created,
denomsSel, denomsSel,
next_url: backupTip.next_url, next_url: backupTip.next_url,
exchangeBaseUrl: backupTip.exchange_base_url, exchangeBaseUrl: backupTip.exchange_base_url,
merchantBaseUrl: backupTip.exchange_base_url, merchantBaseUrl: backupTip.exchange_base_url,
merchantTipId: backupTip.merchant_tip_id, merchantRewardId: backupTip.merchant_tip_id,
pickedUpTimestamp: backupTip.timestamp_finished, pickedUpTimestamp: backupTip.timestamp_finished,
secretSeed: backupTip.secret_seed, secretSeed: backupTip.secret_seed,
tipAmountEffective: Amounts.stringify(denomsSel.totalCoinValue), rewardAmountEffective: Amounts.stringify(denomsSel.totalCoinValue),
tipAmountRaw: Amounts.stringify(tipAmountRaw), rewardAmountRaw: Amounts.stringify(tipAmountRaw),
tipExpiration: backupTip.timestamp_expiration, rewardExpiration: backupTip.timestamp_expiration,
walletTipId: backupTip.wallet_tip_id, walletRewardId: backupTip.wallet_tip_id,
denomSelUid: backupTip.selected_denoms_uid, denomSelUid: backupTip.selected_denoms_uid,
status: TipRecordStatus.Done, // FIXME! status: RewardRecordStatus.Done, // FIXME!
}); });
} }
} }
@ -863,8 +863,8 @@ export async function importBackup(
} else if (type === TombstoneTag.DeleteRefund) { } else if (type === TombstoneTag.DeleteRefund) {
// Nothing required, will just prevent display // Nothing required, will just prevent display
// in the transactions list // in the transactions list
} else if (type === TombstoneTag.DeleteTip) { } else if (type === TombstoneTag.DeleteReward) {
await tx.tips.delete(rest[0]); await tx.rewards.delete(rest[0]);
} else if (type === TombstoneTag.DeleteWithdrawalGroup) { } else if (type === TombstoneTag.DeleteWithdrawalGroup) {
await tx.withdrawalGroups.delete(rest[0]); await tx.withdrawalGroups.delete(rest[0]);
} else { } else {

View File

@ -57,7 +57,7 @@ import {
PurchaseRecord, PurchaseRecord,
RecoupGroupRecord, RecoupGroupRecord,
RefreshGroupRecord, RefreshGroupRecord,
TipRecord, RewardRecord,
WithdrawalGroupRecord, WithdrawalGroupRecord,
} from "../db.js"; } from "../db.js";
import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util"; import { makeErrorDetail, TalerError } from "@gnu-taler/taler-util";
@ -293,10 +293,10 @@ function convertTaskToTransactionId(
tag: TransactionType.Refresh, tag: TransactionType.Refresh,
refreshGroupId: parsedTaskId.refreshGroupId, refreshGroupId: parsedTaskId.refreshGroupId,
}); });
case PendingTaskType.TipPickup: case PendingTaskType.RewardPickup:
return constructTransactionIdentifier({ return constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: parsedTaskId.walletTipId, walletRewardId: parsedTaskId.walletRewardId,
}); });
case PendingTaskType.PeerPushDebit: case PendingTaskType.PeerPushDebit:
return constructTransactionIdentifier({ return constructTransactionIdentifier({
@ -515,7 +515,7 @@ export enum TombstoneTag {
DeleteWithdrawalGroup = "delete-withdrawal-group", DeleteWithdrawalGroup = "delete-withdrawal-group",
DeleteReserve = "delete-reserve", DeleteReserve = "delete-reserve",
DeletePayment = "delete-payment", DeletePayment = "delete-payment",
DeleteTip = "delete-tip", DeleteReward = "delete-reward",
DeleteRefreshGroup = "delete-refresh-group", DeleteRefreshGroup = "delete-refresh-group",
DeleteDepositGroup = "delete-deposit-group", DeleteDepositGroup = "delete-deposit-group",
DeleteRefund = "delete-refund", DeleteRefund = "delete-refund",
@ -601,7 +601,9 @@ export function runLongpollAsync(
}; };
res = await reqFn(cts.token); res = await reqFn(cts.token);
} catch (e) { } catch (e) {
await storePendingTaskError(ws, retryTag, getErrorDetailFromException(e)); const errDetail = getErrorDetailFromException(e);
logger.warn(`got error during long-polling: ${j2s(errDetail)}`);
await storePendingTaskError(ws, retryTag, errDetail);
return; return;
} finally { } finally {
delete ws.activeLongpoll[retryTag]; delete ws.activeLongpoll[retryTag];
@ -622,7 +624,7 @@ export type ParsedTombstone =
| { tag: TombstoneTag.DeleteRefund; refundGroupId: string } | { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
| { tag: TombstoneTag.DeleteReserve; reservePub: string } | { tag: TombstoneTag.DeleteReserve; reservePub: string }
| { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string } | { tag: TombstoneTag.DeleteRefreshGroup; refreshGroupId: string }
| { tag: TombstoneTag.DeleteTip; walletTipId: string } | { tag: TombstoneTag.DeleteReward; walletTipId: string }
| { tag: TombstoneTag.DeletePayment; proposalId: string }; | { tag: TombstoneTag.DeletePayment; proposalId: string };
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr { export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
@ -637,7 +639,7 @@ export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.proposalId}` as TombstoneIdStr;
case TombstoneTag.DeleteRefreshGroup: case TombstoneTag.DeleteRefreshGroup:
return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.refreshGroupId}` as TombstoneIdStr;
case TombstoneTag.DeleteTip: case TombstoneTag.DeleteReward:
return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr; return `tmb:${p.tag}:${p.walletTipId}` as TombstoneIdStr;
default: default:
assertUnreachable(p); assertUnreachable(p);
@ -810,7 +812,7 @@ export type ParsedTaskIdentifier =
| { tag: PendingTaskType.PeerPushDebit; pursePub: string } | { tag: PendingTaskType.PeerPushDebit; pursePub: string }
| { tag: PendingTaskType.Purchase; proposalId: string } | { tag: PendingTaskType.Purchase; proposalId: string }
| { tag: PendingTaskType.Recoup; recoupGroupId: string } | { tag: PendingTaskType.Recoup; recoupGroupId: string }
| { tag: PendingTaskType.TipPickup; walletTipId: string } | { tag: PendingTaskType.RewardPickup; walletRewardId: string }
| { tag: PendingTaskType.Refresh; refreshGroupId: string }; | { tag: PendingTaskType.Refresh; refreshGroupId: string };
export function parseTaskIdentifier(x: string): ParsedTaskIdentifier { export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
@ -844,8 +846,8 @@ export function parseTaskIdentifier(x: string): ParsedTaskIdentifier {
return { tag: type, recoupGroupId: rest[0] }; return { tag: type, recoupGroupId: rest[0] };
case PendingTaskType.Refresh: case PendingTaskType.Refresh:
return { tag: type, refreshGroupId: rest[0] }; return { tag: type, refreshGroupId: rest[0] };
case PendingTaskType.TipPickup: case PendingTaskType.RewardPickup:
return { tag: type, walletTipId: rest[0] }; return { tag: type, walletRewardId: rest[0] };
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return { tag: type, withdrawalGroupId: rest[0] }; return { tag: type, withdrawalGroupId: rest[0] };
default: default:
@ -877,8 +879,8 @@ export function constructTaskIdentifier(p: ParsedTaskIdentifier): TaskId {
return `${p.tag}:${p.recoupGroupId}` as TaskId; return `${p.tag}:${p.recoupGroupId}` as TaskId;
case PendingTaskType.Refresh: case PendingTaskType.Refresh:
return `${p.tag}:${p.refreshGroupId}` as TaskId; return `${p.tag}:${p.refreshGroupId}` as TaskId;
case PendingTaskType.TipPickup: case PendingTaskType.RewardPickup:
return `${p.tag}:${p.walletTipId}` as TaskId; return `${p.tag}:${p.walletRewardId}` as TaskId;
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return `${p.tag}:${p.withdrawalGroupId}` as TaskId; return `${p.tag}:${p.withdrawalGroupId}` as TaskId;
default: default:
@ -899,8 +901,8 @@ export namespace TaskIdentifiers {
export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId { export function forExchangeCheckRefresh(exch: ExchangeRecord): TaskId {
return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId; return `${PendingTaskType.ExchangeCheckRefresh}:${exch.baseUrl}` as TaskId;
} }
export function forTipPickup(tipRecord: TipRecord): TaskId { export function forTipPickup(tipRecord: RewardRecord): TaskId {
return `${PendingTaskType.TipPickup}:${tipRecord.walletTipId}` as TaskId; return `${PendingTaskType.RewardPickup}:${tipRecord.walletRewardId}` as TaskId;
} }
export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId { export function forRefresh(refreshGroupRecord: RefreshGroupRecord): TaskId {
return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId; return `${PendingTaskType.Refresh}:${refreshGroupRecord.refreshGroupId}` as TaskId;

View File

@ -436,16 +436,25 @@ async function handlePeerPullCreditCreatePurse(
logger.info(`reserve merge response: ${j2s(resp)}`); logger.info(`reserve merge response: ${j2s(resp)}`);
await ws.db const transactionId = constructTransactionIdentifier({
tag: TransactionType.PeerPullCredit,
pursePub: pullIni.pursePub,
});
const transitionInfo = await ws.db
.mktx((x) => [x.peerPullPaymentInitiations]) .mktx((x) => [x.peerPullPaymentInitiations])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pi2 = await tx.peerPullPaymentInitiations.get(pursePub); const pi2 = await tx.peerPullPaymentInitiations.get(pursePub);
if (!pi2) { if (!pi2) {
return; return;
} }
const oldTxState = computePeerPullCreditTransactionState(pi2);
pi2.status = PeerPullPaymentInitiationStatus.PendingReady; pi2.status = PeerPullPaymentInitiationStatus.PendingReady;
await tx.peerPullPaymentInitiations.put(pi2); await tx.peerPullPaymentInitiations.put(pi2);
const newTxState = computePeerPullCreditTransactionState(pi2);
return { oldTxState, newTxState };
}); });
notifyTransition(ws, transactionId, transitionInfo);
return TaskRunResult.finished(); return TaskRunResult.finished();
} }

View File

@ -32,7 +32,7 @@ import {
PeerPushPaymentIncomingStatus, PeerPushPaymentIncomingStatus,
PeerPullPaymentInitiationStatus, PeerPullPaymentInitiationStatus,
WithdrawalGroupStatus, WithdrawalGroupStatus,
TipRecordStatus, RewardRecordStatus,
DepositOperationStatus, DepositOperationStatus,
} from "../db.js"; } from "../db.js";
import { import {
@ -232,17 +232,17 @@ async function gatherDepositPending(
async function gatherTipPending( async function gatherTipPending(
ws: InternalWalletState, ws: InternalWalletState,
tx: GetReadOnlyAccess<{ tx: GetReadOnlyAccess<{
tips: typeof WalletStoresV1.tips; rewards: typeof WalletStoresV1.rewards;
operationRetries: typeof WalletStoresV1.operationRetries; operationRetries: typeof WalletStoresV1.operationRetries;
}>, }>,
now: AbsoluteTime, now: AbsoluteTime,
resp: PendingOperationsResponse, resp: PendingOperationsResponse,
): Promise<void> { ): Promise<void> {
const range = GlobalIDB.KeyRange.bound( const range = GlobalIDB.KeyRange.bound(
TipRecordStatus.PendingPickup, RewardRecordStatus.PendingPickup,
TipRecordStatus.PendingPickup, RewardRecordStatus.PendingPickup,
); );
await tx.tips.indexes.byStatus.iter(range).forEachAsync(async (tip) => { await tx.rewards.indexes.byStatus.iter(range).forEachAsync(async (tip) => {
// FIXME: The tip record needs a proper status field! // FIXME: The tip record needs a proper status field!
if (tip.pickedUpTimestamp) { if (tip.pickedUpTimestamp) {
return; return;
@ -252,13 +252,13 @@ async function gatherTipPending(
const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(); const timestampDue = retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now();
if (tip.acceptedTimestamp) { if (tip.acceptedTimestamp) {
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingTaskType.TipPickup, type: PendingTaskType.RewardPickup,
...getPendingCommon(ws, opId, timestampDue), ...getPendingCommon(ws, opId, timestampDue),
givesLifeness: true, givesLifeness: true,
timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(), timestampDue: retryRecord?.retryInfo.nextRetry ?? AbsoluteTime.now(),
merchantBaseUrl: tip.merchantBaseUrl, merchantBaseUrl: tip.merchantBaseUrl,
tipId: tip.walletTipId, tipId: tip.walletRewardId,
merchantTipId: tip.merchantTipId, merchantTipId: tip.merchantRewardId,
}); });
} }
}); });
@ -494,7 +494,7 @@ export async function getPendingOperations(
x.refreshGroups, x.refreshGroups,
x.coins, x.coins,
x.withdrawalGroups, x.withdrawalGroups,
x.tips, x.rewards,
x.purchases, x.purchases,
x.planchets, x.planchets,
x.depositGroups, x.depositGroups,

View File

@ -82,7 +82,7 @@ async function putGroupAsFinished(
await tx.recoupGroups.put(recoupGroup); await tx.recoupGroups.put(recoupGroup);
} }
async function recoupTipCoin( async function recoupRewardCoin(
ws: InternalWalletState, ws: InternalWalletState,
recoupGroupId: string, recoupGroupId: string,
coinIdx: number, coinIdx: number,
@ -482,8 +482,8 @@ async function processRecoup(
const cs = coin.coinSource; const cs = coin.coinSource;
switch (cs.type) { switch (cs.type) {
case CoinSourceType.Tip: case CoinSourceType.Reward:
return recoupTipCoin(ws, recoupGroupId, coinIdx, coin); return recoupRewardCoin(ws, recoupGroupId, coinIdx, coin);
case CoinSourceType.Refresh: case CoinSourceType.Refresh:
return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs); return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
case CoinSourceType.Withdraw: case CoinSourceType.Withdraw:

View File

@ -31,7 +31,7 @@ import {
j2s, j2s,
Logger, Logger,
NotificationType, NotificationType,
parseTipUri, parseRewardUri,
PrepareTipResult, PrepareTipResult,
TalerErrorCode, TalerErrorCode,
TalerPreciseTimestamp, TalerPreciseTimestamp,
@ -48,8 +48,8 @@ import {
CoinRecord, CoinRecord,
CoinSourceType, CoinSourceType,
DenominationRecord, DenominationRecord,
TipRecord, RewardRecord,
TipRecordStatus, RewardRecordStatus,
} from "../db.js"; } from "../db.js";
import { makeErrorDetail } from "@gnu-taler/taler-util"; import { makeErrorDetail } from "@gnu-taler/taler-util";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
@ -84,31 +84,31 @@ const logger = new Logger("operations/tip.ts");
/** /**
* Get the (DD37-style) transaction status based on the * Get the (DD37-style) transaction status based on the
* database record of a tip. * database record of a reward.
*/ */
export function computeTipTransactionStatus( export function computeRewardTransactionStatus(
tipRecord: TipRecord, tipRecord: RewardRecord,
): TransactionState { ): TransactionState {
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
return { return {
major: TransactionMajorState.Done, major: TransactionMajorState.Done,
}; };
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
return { return {
major: TransactionMajorState.Aborted, major: TransactionMajorState.Aborted,
}; };
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
}; };
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
return { return {
major: TransactionMajorState.Dialog, major: TransactionMajorState.Dialog,
minor: TransactionMinorState.Proposed, minor: TransactionMinorState.Proposed,
}; };
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return { return {
major: TransactionMajorState.Pending, major: TransactionMajorState.Pending,
minor: TransactionMinorState.Pickup, minor: TransactionMinorState.Pickup,
@ -119,18 +119,18 @@ export function computeTipTransactionStatus(
} }
export function computeTipTransactionActions( export function computeTipTransactionActions(
tipRecord: TipRecord, tipRecord: RewardRecord,
): TransactionAction[] { ): TransactionAction[] {
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
return [TransactionAction.Suspend, TransactionAction.Fail]; return [TransactionAction.Suspend, TransactionAction.Fail];
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return [TransactionAction.Resume, TransactionAction.Fail]; return [TransactionAction.Resume, TransactionAction.Fail];
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
return [TransactionAction.Abort]; return [TransactionAction.Abort];
default: default:
assertUnreachable(tipRecord.status); assertUnreachable(tipRecord.status);
@ -141,15 +141,15 @@ export async function prepareTip(
ws: InternalWalletState, ws: InternalWalletState,
talerTipUri: string, talerTipUri: string,
): Promise<PrepareTipResult> { ): Promise<PrepareTipResult> {
const res = parseTipUri(talerTipUri); const res = parseRewardUri(talerTipUri);
if (!res) { if (!res) {
throw Error("invalid taler://tip URI"); throw Error("invalid taler://tip URI");
} }
let tipRecord = await ws.db let tipRecord = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.tips.indexes.byMerchantTipIdAndBaseUrl.get([ return tx.rewards.indexes.byMerchantTipIdAndBaseUrl.get([
res.merchantTipId, res.merchantTipId,
res.merchantBaseUrl, res.merchantBaseUrl,
]); ]);
@ -194,44 +194,44 @@ export async function prepareTip(
const secretSeed = encodeCrock(getRandomBytes(64)); const secretSeed = encodeCrock(getRandomBytes(64));
const denomSelUid = encodeCrock(getRandomBytes(32)); const denomSelUid = encodeCrock(getRandomBytes(32));
const newTipRecord: TipRecord = { const newTipRecord: RewardRecord = {
walletTipId: walletTipId, walletRewardId: walletTipId,
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
status: TipRecordStatus.DialogAccept, status: RewardRecordStatus.DialogAccept,
tipAmountRaw: Amounts.stringify(amount), rewardAmountRaw: Amounts.stringify(amount),
tipExpiration: tipPickupStatus.expiration, rewardExpiration: tipPickupStatus.expiration,
exchangeBaseUrl: tipPickupStatus.exchange_url, exchangeBaseUrl: tipPickupStatus.exchange_url,
next_url: tipPickupStatus.next_url, next_url: tipPickupStatus.next_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
createdTimestamp: TalerPreciseTimestamp.now(), createdTimestamp: TalerPreciseTimestamp.now(),
merchantTipId: res.merchantTipId, merchantRewardId: res.merchantTipId,
tipAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), rewardAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
denomsSel: selectedDenoms, denomsSel: selectedDenoms,
pickedUpTimestamp: undefined, pickedUpTimestamp: undefined,
secretSeed, secretSeed,
denomSelUid, denomSelUid,
}; };
await ws.db await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.tips.put(newTipRecord); await tx.rewards.put(newTipRecord);
}); });
tipRecord = newTipRecord; tipRecord = newTipRecord;
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
}); });
const tipStatus: PrepareTipResult = { const tipStatus: PrepareTipResult = {
accepted: !!tipRecord && !!tipRecord.acceptedTimestamp, accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
tipAmountRaw: Amounts.stringify(tipRecord.tipAmountRaw), rewardAmountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
exchangeBaseUrl: tipRecord.exchangeBaseUrl, exchangeBaseUrl: tipRecord.exchangeBaseUrl,
merchantBaseUrl: tipRecord.merchantBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl,
expirationTimestamp: tipRecord.tipExpiration, expirationTimestamp: tipRecord.rewardExpiration,
tipAmountEffective: Amounts.stringify(tipRecord.tipAmountEffective), rewardAmountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
transactionId, transactionId,
}; };
@ -243,25 +243,25 @@ export async function processTip(
walletTipId: string, walletTipId: string,
): Promise<TaskRunResult> { ): Promise<TaskRunResult> {
const tipRecord = await ws.db const tipRecord = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.tips.get(walletTipId); return tx.rewards.get(walletTipId);
}); });
if (!tipRecord) { if (!tipRecord) {
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
switch (tipRecord.status) { switch (tipRecord.status) {
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
return TaskRunResult.finished(); return TaskRunResult.finished();
} }
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletTipId,
}); });
const denomsForWithdraw = tipRecord.denomsSel; const denomsForWithdraw = tipRecord.denomsSel;
@ -300,7 +300,7 @@ export async function processTip(
} }
const tipStatusUrl = new URL( const tipStatusUrl = new URL(
`tips/${tipRecord.merchantTipId}/pickup`, `tips/${tipRecord.merchantRewardId}/pickup`,
tipRecord.merchantBaseUrl, tipRecord.merchantBaseUrl,
); );
@ -384,9 +384,9 @@ export async function processTip(
coinPriv: planchet.coinPriv, coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub, coinPub: planchet.coinPub,
coinSource: { coinSource: {
type: CoinSourceType.Tip, type: CoinSourceType.Reward,
coinIndex: i, coinIndex: i,
walletTipId: walletTipId, walletRewardId: walletTipId,
}, },
sourceTransactionId: transactionId, sourceTransactionId: transactionId,
denomPubHash: denom.denomPubHash, denomPubHash: denom.denomPubHash,
@ -401,20 +401,20 @@ export async function processTip(
} }
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.tips]) .mktx((x) => [x.coins, x.coinAvailability, x.denominations, x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tr = await tx.tips.get(walletTipId); const tr = await tx.rewards.get(walletTipId);
if (!tr) { if (!tr) {
return; return;
} }
if (tr.status !== TipRecordStatus.PendingPickup) { if (tr.status !== RewardRecordStatus.PendingPickup) {
return; return;
} }
const oldTxState = computeTipTransactionStatus(tr); const oldTxState = computeRewardTransactionStatus(tr);
tr.pickedUpTimestamp = TalerPreciseTimestamp.now(); tr.pickedUpTimestamp = TalerPreciseTimestamp.now();
tr.status = TipRecordStatus.Done; tr.status = RewardRecordStatus.Done;
await tx.tips.put(tr); await tx.rewards.put(tr);
const newTxState = computeTipTransactionStatus(tr); const newTxState = computeRewardTransactionStatus(tr);
for (const cr of newCoinRecords) { for (const cr of newCoinRecords) {
await makeCoinAvailable(ws, tx, cr); await makeCoinAvailable(ws, tx, cr);
} }
@ -432,26 +432,26 @@ export async function acceptTip(
walletTipId: string, walletTipId: string,
): Promise<AcceptTipResponse> { ): Promise<AcceptTipResponse> {
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletTipId,
}); });
const dbRes = await ws.db const dbRes = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(walletTipId); const tipRecord = await tx.rewards.get(walletTipId);
if (!tipRecord) { if (!tipRecord) {
logger.error("tip not found"); logger.error("tip not found");
return; return;
} }
if (tipRecord.status != TipRecordStatus.DialogAccept) { if (tipRecord.status != RewardRecordStatus.DialogAccept) {
logger.warn("Unable to accept tip in the current state"); logger.warn("Unable to accept tip in the current state");
return { tipRecord }; return { tipRecord };
} }
const oldTxState = computeTipTransactionStatus(tipRecord); const oldTxState = computeRewardTransactionStatus(tipRecord);
tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now(); tipRecord.acceptedTimestamp = TalerPreciseTimestamp.now();
tipRecord.status = TipRecordStatus.PendingPickup; tipRecord.status = RewardRecordStatus.PendingPickup;
await tx.tips.put(tipRecord); await tx.rewards.put(tipRecord);
const newTxState = computeTipTransactionStatus(tipRecord); const newTxState = computeRewardTransactionStatus(tipRecord);
return { tipRecord, transitionInfo: { oldTxState, newTxState } }; return { tipRecord, transitionInfo: { oldTxState, newTxState } };
}); });
@ -465,53 +465,53 @@ export async function acceptTip(
return { return {
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: walletTipId, walletRewardId: walletTipId,
}), }),
next_url: tipRecord.next_url, next_url: tipRecord.next_url,
}; };
} }
export async function suspendTipTransaction( export async function suspendRewardTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const tipRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction tip ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
newStatus = TipRecordStatus.SuspendidPickup; newStatus = RewardRecordStatus.SuspendidPickup;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(tipRec);
await tx.tips.put(tipRec); await tx.rewards.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,
@ -525,43 +525,43 @@ export async function suspendTipTransaction(
export async function resumeTipTransaction( export async function resumeTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const rewardRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!rewardRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction reward ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (rewardRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
newStatus = TipRecordStatus.PendingPickup; newStatus = RewardRecordStatus.PendingPickup;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(rewardRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(rewardRec);
tipRec.status = newStatus; rewardRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(rewardRec);
await tx.tips.put(tipRec); await tx.rewards.put(rewardRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,
@ -582,43 +582,43 @@ export async function failTipTransaction(
export async function abortTipTransaction( export async function abortTipTransaction(
ws: InternalWalletState, ws: InternalWalletState,
walletTipId: string, walletRewardId: string,
): Promise<void> { ): Promise<void> {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId, walletRewardId: walletRewardId,
}); });
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId, walletRewardId: walletRewardId,
}); });
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.tips]) .mktx((x) => [x.rewards])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRec = await tx.tips.get(walletTipId); const tipRec = await tx.rewards.get(walletRewardId);
if (!tipRec) { if (!tipRec) {
logger.warn(`transaction tip ${walletTipId} not found`); logger.warn(`transaction tip ${walletRewardId} not found`);
return; return;
} }
let newStatus: TipRecordStatus | undefined = undefined; let newStatus: RewardRecordStatus | undefined = undefined;
switch (tipRec.status) { switch (tipRec.status) {
case TipRecordStatus.Done: case RewardRecordStatus.Done:
case TipRecordStatus.Aborted: case RewardRecordStatus.Aborted:
case TipRecordStatus.PendingPickup: case RewardRecordStatus.PendingPickup:
case TipRecordStatus.DialogAccept: case RewardRecordStatus.DialogAccept:
break; break;
case TipRecordStatus.SuspendidPickup: case RewardRecordStatus.SuspendidPickup:
newStatus = TipRecordStatus.Aborted; newStatus = RewardRecordStatus.Aborted;
break; break;
default: default:
assertUnreachable(tipRec.status); assertUnreachable(tipRec.status);
} }
if (newStatus != null) { if (newStatus != null) {
const oldTxState = computeTipTransactionStatus(tipRec); const oldTxState = computeRewardTransactionStatus(tipRec);
tipRec.status = newStatus; tipRec.status = newStatus;
const newTxState = computeTipTransactionStatus(tipRec); const newTxState = computeRewardTransactionStatus(tipRec);
await tx.tips.put(tipRec); await tx.rewards.put(tipRec);
return { return {
oldTxState, oldTxState,
newTxState, newTxState,

View File

@ -58,7 +58,7 @@ import {
RefreshGroupRecord, RefreshGroupRecord,
RefreshOperationStatus, RefreshOperationStatus,
RefundGroupRecord, RefundGroupRecord,
TipRecord, RewardRecord,
WalletContractData, WalletContractData,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WithdrawalGroupStatus, WithdrawalGroupStatus,
@ -107,11 +107,11 @@ import {
import { import {
abortTipTransaction, abortTipTransaction,
failTipTransaction, failTipTransaction,
computeTipTransactionStatus, computeRewardTransactionStatus,
resumeTipTransaction, resumeTipTransaction,
suspendTipTransaction, suspendRewardTransaction,
computeTipTransactionActions, computeTipTransactionActions,
} from "./tip.js"; } from "./reward.js";
import { import {
abortWithdrawalTransaction, abortWithdrawalTransaction,
augmentPaytoUrisForWithdrawal, augmentPaytoUrisForWithdrawal,
@ -187,7 +187,7 @@ function shouldSkipSearch(
*/ */
const txOrder: { [t in TransactionType]: number } = { const txOrder: { [t in TransactionType]: number } = {
[TransactionType.Withdrawal]: 1, [TransactionType.Withdrawal]: 1,
[TransactionType.Tip]: 2, [TransactionType.Reward]: 2,
[TransactionType.Payment]: 3, [TransactionType.Payment]: 3,
[TransactionType.PeerPullCredit]: 4, [TransactionType.PeerPullCredit]: 4,
[TransactionType.PeerPullDebit]: 5, [TransactionType.PeerPullDebit]: 5,
@ -284,12 +284,12 @@ export async function getTransactionById(
throw Error(`no tx for refresh`); throw Error(`no tx for refresh`);
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const tipId = parsedTx.walletTipId; const tipId = parsedTx.walletRewardId;
return await ws.db return await ws.db
.mktx((x) => [x.tips, x.operationRetries]) .mktx((x) => [x.rewards, x.operationRetries])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId); const tipRecord = await tx.rewards.get(tipId);
if (!tipRecord) throw Error("not found"); if (!tipRecord) throw Error("not found");
const retries = await tx.operationRetries.get( const retries = await tx.operationRetries.get(
@ -818,21 +818,21 @@ function buildTransactionForDeposit(
} }
function buildTransactionForTip( function buildTransactionForTip(
tipRecord: TipRecord, tipRecord: RewardRecord,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Transaction { ): Transaction {
checkLogicInvariant(!!tipRecord.acceptedTimestamp); checkLogicInvariant(!!tipRecord.acceptedTimestamp);
return { return {
type: TransactionType.Tip, type: TransactionType.Reward,
txState: computeTipTransactionStatus(tipRecord), txState: computeRewardTransactionStatus(tipRecord),
txActions: computeTipTransactionActions(tipRecord), txActions: computeTipTransactionActions(tipRecord),
amountEffective: Amounts.stringify(tipRecord.tipAmountEffective), amountEffective: Amounts.stringify(tipRecord.rewardAmountEffective),
amountRaw: Amounts.stringify(tipRecord.tipAmountRaw), amountRaw: Amounts.stringify(tipRecord.rewardAmountRaw),
timestamp: tipRecord.acceptedTimestamp, timestamp: tipRecord.acceptedTimestamp,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: tipRecord.walletTipId, walletRewardId: tipRecord.walletRewardId,
}), }),
merchantBaseUrl: tipRecord.merchantBaseUrl, merchantBaseUrl: tipRecord.merchantBaseUrl,
...(ort?.lastError ? { error: ort.lastError } : {}), ...(ort?.lastError ? { error: ort.lastError } : {}),
@ -945,7 +945,7 @@ export async function getTransactions(
x.purchases, x.purchases,
x.contractTerms, x.contractTerms,
x.recoupGroups, x.recoupGroups,
x.tips, x.rewards,
x.tombstones, x.tombstones,
x.withdrawalGroups, x.withdrawalGroups,
x.refreshGroups, x.refreshGroups,
@ -1200,11 +1200,11 @@ export async function getTransactions(
); );
}); });
tx.tips.iter().forEachAsync(async (tipRecord) => { tx.rewards.iter().forEachAsync(async (tipRecord) => {
if ( if (
shouldSkipCurrency( shouldSkipCurrency(
transactionsRequest, transactionsRequest,
Amounts.parseOrThrow(tipRecord.tipAmountRaw).currency, Amounts.parseOrThrow(tipRecord.rewardAmountRaw).currency,
) )
) { ) {
return; return;
@ -1267,7 +1267,7 @@ export type ParsedTransactionIdentifier =
| { tag: TransactionType.PeerPushDebit; pursePub: string } | { tag: TransactionType.PeerPushDebit; pursePub: string }
| { tag: TransactionType.Refresh; refreshGroupId: string } | { tag: TransactionType.Refresh; refreshGroupId: string }
| { tag: TransactionType.Refund; refundGroupId: string } | { tag: TransactionType.Refund; refundGroupId: string }
| { tag: TransactionType.Tip; walletTipId: string } | { tag: TransactionType.Reward; walletRewardId: string }
| { tag: TransactionType.Withdrawal; withdrawalGroupId: string } | { tag: TransactionType.Withdrawal; withdrawalGroupId: string }
| { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string }; | { tag: TransactionType.InternalWithdrawal; withdrawalGroupId: string };
@ -1291,8 +1291,8 @@ export function constructTransactionIdentifier(
return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.refreshGroupId}` as TransactionIdStr;
case TransactionType.Refund: case TransactionType.Refund:
return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.refundGroupId}` as TransactionIdStr;
case TransactionType.Tip: case TransactionType.Reward:
return `txn:${pTxId.tag}:${pTxId.walletTipId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.walletRewardId}` as TransactionIdStr;
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr; return `txn:${pTxId.tag}:${pTxId.withdrawalGroupId}` as TransactionIdStr;
case TransactionType.InternalWithdrawal: case TransactionType.InternalWithdrawal:
@ -1346,10 +1346,10 @@ export function parseTransactionIdentifier(
tag: TransactionType.Refund, tag: TransactionType.Refund,
refundGroupId: rest[0], refundGroupId: rest[0],
}; };
case TransactionType.Tip: case TransactionType.Reward:
return { return {
tag: TransactionType.Tip, tag: TransactionType.Reward,
walletTipId: rest[0], walletRewardId: rest[0],
}; };
case TransactionType.Withdrawal: case TransactionType.Withdrawal:
return { return {
@ -1427,10 +1427,10 @@ export async function retryTransaction(
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
break; break;
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const taskId = constructTaskIdentifier({ const taskId = constructTaskIdentifier({
tag: PendingTaskType.TipPickup, tag: PendingTaskType.RewardPickup,
walletTipId: parsedTx.walletTipId, walletRewardId: parsedTx.walletRewardId,
}); });
await resetPendingTaskTimeout(ws, taskId); await resetPendingTaskTimeout(ws, taskId);
stopLongpolling(ws, taskId); stopLongpolling(ws, taskId);
@ -1522,8 +1522,8 @@ export async function suspendTransaction(
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("refund transactions can't be suspended or resumed"); throw Error("refund transactions can't be suspended or resumed");
case TransactionType.Tip: case TransactionType.Reward:
await suspendTipTransaction(ws, tx.walletTipId); await suspendRewardTransaction(ws, tx.walletRewardId);
break; break;
default: default:
assertUnreachable(tx); assertUnreachable(tx);
@ -1551,8 +1551,8 @@ export async function failTransaction(
return; return;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("can't do cancel-aborting on refund transaction"); throw Error("can't do cancel-aborting on refund transaction");
case TransactionType.Tip: case TransactionType.Reward:
await failTipTransaction(ws, tx.walletTipId); await failTipTransaction(ws, tx.walletRewardId);
return; return;
case TransactionType.Refresh: case TransactionType.Refresh:
await failRefreshGroup(ws, tx.refreshGroupId); await failRefreshGroup(ws, tx.refreshGroupId);
@ -1613,8 +1613,8 @@ export async function resumeTransaction(
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("refund transactions can't be suspended or resumed"); throw Error("refund transactions can't be suspended or resumed");
case TransactionType.Tip: case TransactionType.Reward:
await resumeTipTransaction(ws, tx.walletTipId); await resumeTipTransaction(ws, tx.walletRewardId);
break; break;
} }
} }
@ -1763,16 +1763,16 @@ export async function deleteTransaction(
return; return;
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const tipId = parsedTx.walletTipId; const tipId = parsedTx.walletRewardId;
await ws.db await ws.db
.mktx((x) => [x.tips, x.tombstones]) .mktx((x) => [x.rewards, x.tombstones])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const tipRecord = await tx.tips.get(tipId); const tipRecord = await tx.rewards.get(tipId);
if (tipRecord) { if (tipRecord) {
await tx.tips.delete(tipId); await tx.rewards.delete(tipId);
await tx.tombstones.put({ await tx.tombstones.put({
id: TombstoneTag.DeleteTip + ":" + tipId, id: TombstoneTag.DeleteReward + ":" + tipId,
}); });
} }
}); });
@ -1856,8 +1856,8 @@ export async function abortTransaction(
case TransactionType.Deposit: case TransactionType.Deposit:
await abortDepositGroup(ws, txId.depositGroupId); await abortDepositGroup(ws, txId.depositGroupId);
break; break;
case TransactionType.Tip: case TransactionType.Reward:
await abortTipTransaction(ws, txId.walletTipId); await abortTipTransaction(ws, txId.walletRewardId);
break; break;
case TransactionType.Refund: case TransactionType.Refund:
throw Error("can't abort refund transactions"); throw Error("can't abort refund transactions");

View File

@ -33,7 +33,7 @@ export enum PendingTaskType {
Purchase = "purchase", Purchase = "purchase",
Refresh = "refresh", Refresh = "refresh",
Recoup = "recoup", Recoup = "recoup",
TipPickup = "tip-pickup", RewardPickup = "reward-pickup",
Withdraw = "withdraw", Withdraw = "withdraw",
Deposit = "deposit", Deposit = "deposit",
Backup = "backup", Backup = "backup",
@ -144,7 +144,7 @@ export interface PendingRefreshTask {
* The wallet is picking up a tip that the user has accepted. * The wallet is picking up a tip that the user has accepted.
*/ */
export interface PendingTipPickupTask { export interface PendingTipPickupTask {
type: PendingTaskType.TipPickup; type: PendingTaskType.RewardPickup;
tipId: string; tipId: string;
merchantBaseUrl: string; merchantBaseUrl: string;
merchantTipId: string; merchantTipId: string;

View File

@ -34,3 +34,11 @@ export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
* Uses libtool's current:revision:age versioning. * Uses libtool's current:revision:age versioning.
*/ */
export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0"; export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = "0:0:0";
/**
* Semver of the wallet-core implementation.
* Will be replaced with the value from package.json in a
* post-compilation step (inside lib/).
*/
export const WALLET_CORE_IMPLEMENTATION_VERSION =
"__WALLET_CORE_IMPLEMENTATION_VERSION__";

View File

@ -29,7 +29,7 @@ import {
AcceptExchangeTosRequest, AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest, AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult, AcceptManualWithdrawalResult,
AcceptTipRequest, AcceptRewardRequest,
AcceptTipResponse, AcceptTipResponse,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
AddExchangeRequest, AddExchangeRequest,
@ -85,8 +85,8 @@ import {
PreparePeerPushCreditRequest, PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse, PreparePeerPushCreditResponse,
PrepareRefundRequest, PrepareRefundRequest,
PrepareTipRequest, PrepareRewardRequest as PrepareRewardRequest,
PrepareTipResult, PrepareTipResult as PrepareRewardResult,
RecoveryLoadRequest, RecoveryLoadRequest,
RetryTransactionRequest, RetryTransactionRequest,
SetCoinSuspendedRequest, SetCoinSuspendedRequest,
@ -178,8 +178,8 @@ export enum WalletApiOperation {
DumpCoins = "dumpCoins", DumpCoins = "dumpCoins",
SetCoinSuspended = "setCoinSuspended", SetCoinSuspended = "setCoinSuspended",
ForceRefresh = "forceRefresh", ForceRefresh = "forceRefresh",
PrepareTip = "prepareTip", PrepareReward = "prepareReward",
AcceptTip = "acceptTip", AcceptReward = "acceptReward",
ExportBackup = "exportBackup", ExportBackup = "exportBackup",
AddBackupProvider = "addBackupProvider", AddBackupProvider = "addBackupProvider",
RemoveBackupProvider = "removeBackupProvider", RemoveBackupProvider = "removeBackupProvider",
@ -507,23 +507,23 @@ export type StartRefundQueryOp = {
response: EmptyObject; response: EmptyObject;
}; };
// group: Tipping // group: Rewards
/** /**
* Query and store information about a tip. * Query and store information about a reward.
*/ */
export type PrepareTipOp = { export type PrepareTipOp = {
op: WalletApiOperation.PrepareTip; op: WalletApiOperation.PrepareReward;
request: PrepareTipRequest; request: PrepareRewardRequest;
response: PrepareTipResult; response: PrepareRewardResult;
}; };
/** /**
* Accept a tip. * Accept a reward.
*/ */
export type AcceptTipOp = { export type AcceptTipOp = {
op: WalletApiOperation.AcceptTip; op: WalletApiOperation.AcceptReward;
request: AcceptTipRequest; request: AcceptRewardRequest;
response: AcceptTipResponse; response: AcceptTipResponse;
}; };
@ -1023,8 +1023,8 @@ export type WalletOperations = {
[WalletApiOperation.ForceRefresh]: ForceRefreshOp; [WalletApiOperation.ForceRefresh]: ForceRefreshOp;
[WalletApiOperation.DeleteTransaction]: DeleteTransactionOp; [WalletApiOperation.DeleteTransaction]: DeleteTransactionOp;
[WalletApiOperation.RetryTransaction]: RetryTransactionOp; [WalletApiOperation.RetryTransaction]: RetryTransactionOp;
[WalletApiOperation.PrepareTip]: PrepareTipOp; [WalletApiOperation.PrepareReward]: PrepareTipOp;
[WalletApiOperation.AcceptTip]: AcceptTipOp; [WalletApiOperation.AcceptReward]: AcceptTipOp;
[WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp; [WalletApiOperation.StartRefundQueryForUri]: StartRefundQueryForUriOp;
[WalletApiOperation.StartRefundQuery]: StartRefundQueryOp; [WalletApiOperation.StartRefundQuery]: StartRefundQueryOp;
[WalletApiOperation.ListCurrencies]: ListCurrenciesOp; [WalletApiOperation.ListCurrencies]: ListCurrenciesOp;

View File

@ -93,7 +93,7 @@ import {
codecForPreparePeerPullPaymentRequest, codecForPreparePeerPullPaymentRequest,
codecForPreparePeerPushCreditRequest, codecForPreparePeerPushCreditRequest,
codecForPrepareRefundRequest, codecForPrepareRefundRequest,
codecForPrepareTipRequest, codecForPrepareRewardRequest,
codecForResumeTransaction, codecForResumeTransaction,
codecForRetryTransactionRequest, codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest, codecForSetCoinSuspendedRequest,
@ -249,10 +249,10 @@ import {
} from "./operations/testing.js"; } from "./operations/testing.js";
import { import {
acceptTip, acceptTip,
computeTipTransactionStatus, computeRewardTransactionStatus,
prepareTip, prepareTip,
processTip, processTip,
} from "./operations/tip.js"; } from "./operations/reward.js";
import { import {
abortTransaction, abortTransaction,
deleteTransaction, deleteTransaction,
@ -300,6 +300,7 @@ import {
import { TimerAPI, TimerGroup } from "./util/timer.js"; import { TimerAPI, TimerGroup } from "./util/timer.js";
import { import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_CORE_IMPLEMENTATION_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
WALLET_MERCHANT_PROTOCOL_VERSION, WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js"; } from "./versions.js";
@ -328,7 +329,7 @@ async function callOperationHandler(
return await processRefreshGroup(ws, pending.refreshGroupId); return await processRefreshGroup(ws, pending.refreshGroupId);
case PendingTaskType.Withdraw: case PendingTaskType.Withdraw:
return await processWithdrawalGroup(ws, pending.withdrawalGroupId); return await processWithdrawalGroup(ws, pending.withdrawalGroupId);
case PendingTaskType.TipPickup: case PendingTaskType.RewardPickup:
return await processTip(ws, pending.tipId); return await processTip(ws, pending.tipId);
case PendingTaskType.Purchase: case PendingTaskType.Purchase:
return await processPurchase(ws, pending.proposalId); return await processPurchase(ws, pending.proposalId);
@ -1016,12 +1017,6 @@ export async function getClientFromWalletState(
return client; return client;
} }
declare const __VERSION__: string;
declare const __GIT_HASH__: string;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : "dev";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
/** /**
* Implementation of the "wallet-core" API. * Implementation of the "wallet-core" API.
*/ */
@ -1355,9 +1350,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
refreshGroupId, refreshGroupId,
}; };
} }
case WalletApiOperation.PrepareTip: { case WalletApiOperation.PrepareReward: {
const req = codecForPrepareTipRequest().decode(payload); const req = codecForPrepareRewardRequest().decode(payload);
return await prepareTip(ws, req.talerTipUri); return await prepareTip(ws, req.talerRewardUri);
} }
case WalletApiOperation.StartRefundQueryForUri: { case WalletApiOperation.StartRefundQueryForUri: {
const req = codecForPrepareRefundRequest().decode(payload); const req = codecForPrepareRefundRequest().decode(payload);
@ -1375,9 +1370,9 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await startQueryRefund(ws, txIdParsed.proposalId); await startQueryRefund(ws, txIdParsed.proposalId);
return {}; return {};
} }
case WalletApiOperation.AcceptTip: { case WalletApiOperation.AcceptReward: {
const req = codecForAcceptTipRequest().decode(payload); const req = codecForAcceptTipRequest().decode(payload);
return await acceptTip(ws, req.walletTipId); return await acceptTip(ws, req.walletRewardId);
} }
case WalletApiOperation.ExportBackupPlain: { case WalletApiOperation.ExportBackupPlain: {
return exportBackup(ws); return exportBackup(ws);
@ -1590,15 +1585,15 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
} }
export function getVersion(ws: InternalWalletState): WalletCoreVersion { export function getVersion(ws: InternalWalletState): WalletCoreVersion {
const version: WalletCoreVersion = { const result: WalletCoreVersion = {
hash: GIT_HASH, hash: undefined,
version: VERSION, version: WALLET_CORE_IMPLEMENTATION_VERSION,
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION, exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION, merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION, bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
devMode: false, devMode: false,
}; };
return version; return result;
} }
/** /**
@ -1889,12 +1884,12 @@ class InternalWalletStateImpl implements InternalWalletState {
} }
return computeRefreshTransactionState(rec); return computeRefreshTransactionState(rec);
} }
case TransactionType.Tip: { case TransactionType.Reward: {
const rec = await tx.tips.get(parsedTxId.walletTipId); const rec = await tx.rewards.get(parsedTxId.walletRewardId);
if (!rec) { if (!rec) {
return undefined; return undefined;
} }
return computeTipTransactionStatus(rec); return computeRewardTransactionStatus(rec);
} }
default: default:
assertUnreachable(parsedTxId); assertUnreachable(parsedTxId);

View File

@ -7,8 +7,10 @@
"target": "ES2017", "target": "ES2017",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node16",
"resolveJsonModule": true,
"sourceMap": true, "sourceMap": true,
"lib": ["es6"], "lib": ["es6"],
"resolvePackageJsonImports": true,
"types": ["node"], "types": ["node"],
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
@ -31,5 +33,8 @@
"path": "../taler-util/" "path": "../taler-util/"
} }
], ],
"include": ["src/**/*"] "include": [
"src/**/*",
"src/*.json"
]
} }

View File

@ -146,7 +146,7 @@ const talerUriActionToPageName: {
} = { } = {
[TalerUriAction.Withdraw]: "ctaWithdraw", [TalerUriAction.Withdraw]: "ctaWithdraw",
[TalerUriAction.Pay]: "ctaPay", [TalerUriAction.Pay]: "ctaPay",
[TalerUriAction.Tip]: "ctaTips", [TalerUriAction.Reward]: "ctaTips",
[TalerUriAction.Refund]: "ctaRefund", [TalerUriAction.Refund]: "ctaRefund",
[TalerUriAction.PayPull]: "ctaInvoicePay", [TalerUriAction.PayPull]: "ctaInvoicePay",
[TalerUriAction.PayPush]: "ctaTransferPickup", [TalerUriAction.PayPush]: "ctaTransferPickup",

View File

@ -134,7 +134,7 @@ export function HistoryItem(props: { tx: Transaction }): VNode {
} }
/> />
); );
case TransactionType.Tip: case TransactionType.Reward:
return ( return (
<Layout <Layout
id={tx.transactionId} id={tx.transactionId}

View File

@ -23,7 +23,7 @@ import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { Props, State } from "./index.js"; import { Props, State } from "./index.js";
export function useComponentState({ export function useComponentState({
talerTipUri, talerTipUri: talerRewardUri,
onCancel, onCancel,
onSuccess, onSuccess,
}: Props): State { }: Props): State {
@ -31,9 +31,9 @@ export function useComponentState({
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const { pushAlertOnError } = useAlertContext(); const { pushAlertOnError } = useAlertContext();
const tipInfo = useAsyncAsHook(async () => { const tipInfo = useAsyncAsHook(async () => {
if (!talerTipUri) throw Error("ERROR_NO-URI-FOR-TIP"); if (!talerRewardUri) throw Error("ERROR_NO-URI-FOR-TIP");
const tip = await api.wallet.call(WalletApiOperation.PrepareTip, { const tip = await api.wallet.call(WalletApiOperation.PrepareReward, {
talerTipUri, talerRewardUri,
}); });
return { tip }; return { tip };
}); });
@ -63,8 +63,8 @@ export function useComponentState({
const { tip } = tipInfo.response; const { tip } = tipInfo.response;
const doAccept = async (): Promise<void> => { const doAccept = async (): Promise<void> => {
const res = await api.wallet.call(WalletApiOperation.AcceptTip, { const res = await api.wallet.call(WalletApiOperation.AcceptReward, {
walletTipId: tip.walletTipId, walletRewardId: tip.walletRewardId,
}); });
//FIX: this may not be seen since we are moving to the success also //FIX: this may not be seen since we are moving to the success also
@ -75,7 +75,7 @@ export function useComponentState({
const baseInfo = { const baseInfo = {
merchantBaseUrl: tip.merchantBaseUrl, merchantBaseUrl: tip.merchantBaseUrl,
exchangeBaseUrl: tip.exchangeBaseUrl, exchangeBaseUrl: tip.exchangeBaseUrl,
amount: Amounts.parseOrThrow(tip.tipAmountEffective), amount: Amounts.parseOrThrow(tip.rewardAmountEffective),
error: undefined, error: undefined,
cancel: { cancel: {
onClick: pushAlertOnError(onCancel), onClick: pushAlertOnError(onCancel),

View File

@ -62,17 +62,17 @@ describe("Tip CTA states", () => {
it("should be ready for accepting the tip", async () => { it("should be ready for accepting the tip", async () => {
const { handler, TestingContext } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
accepted: false, accepted: false,
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", rewardAmountEffective: "EUR:1",
walletTipId: "tip_id", walletRewardId: "tip_id",
transactionId: "txn:tip:ABC1234", transactionId: "txn:tip:ABC1234",
expirationTimestamp: { expirationTimestamp: {
t_s: 1, t_s: 1,
}, },
tipAmountRaw: "", rewardAmountRaw: "",
}); });
const props: Props = { const props: Props = {
@ -100,23 +100,23 @@ describe("Tip CTA states", () => {
expect(state.exchangeBaseUrl).eq("exchange url"); expect(state.exchangeBaseUrl).eq("exchange url");
if (state.accept.onClick === undefined) expect.fail(); if (state.accept.onClick === undefined) expect.fail();
handler.addWalletCallResponse(WalletApiOperation.AcceptTip); handler.addWalletCallResponse(WalletApiOperation.AcceptReward);
state.accept.onClick(); state.accept.onClick();
handler.addWalletCallResponse( handler.addWalletCallResponse(
WalletApiOperation.PrepareTip, WalletApiOperation.PrepareReward,
undefined, undefined,
{ {
accepted: true, accepted: true,
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", rewardAmountEffective: "EUR:1",
walletTipId: "tip_id", walletRewardId: "tip_id",
transactionId: "txn:tip:ABC1234", transactionId: "txn:tip:ABC1234",
expirationTimestamp: { expirationTimestamp: {
t_s: 1, t_s: 1,
}, },
tipAmountRaw: "", rewardAmountRaw: "",
}, },
); );
}, },
@ -137,17 +137,17 @@ describe("Tip CTA states", () => {
it.skip("should be ignored after clicking the ignore button", async () => { it.skip("should be ignored after clicking the ignore button", async () => {
const { handler, TestingContext } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", rewardAmountEffective: "EUR:1",
walletTipId: "tip_id", walletRewardId: "tip_id",
transactionId: "txn:tip:ABC1234", transactionId: "txn:tip:ABC1234",
accepted: false, accepted: false,
expirationTimestamp: { expirationTimestamp: {
t_s: 1, t_s: 1,
}, },
tipAmountRaw: "", rewardAmountRaw: "",
}); });
const props: Props = { const props: Props = {
@ -184,17 +184,17 @@ describe("Tip CTA states", () => {
it("should render accepted if the tip has been used previously", async () => { it("should render accepted if the tip has been used previously", async () => {
const { handler, TestingContext } = createWalletApiMock(); const { handler, TestingContext } = createWalletApiMock();
handler.addWalletCallResponse(WalletApiOperation.PrepareTip, undefined, { handler.addWalletCallResponse(WalletApiOperation.PrepareReward, undefined, {
accepted: true, accepted: true,
exchangeBaseUrl: "exchange url", exchangeBaseUrl: "exchange url",
merchantBaseUrl: "merchant url", merchantBaseUrl: "merchant url",
tipAmountEffective: "EUR:1", rewardAmountEffective: "EUR:1",
walletTipId: "tip_id", walletRewardId: "tip_id",
transactionId: "txn:tip:ABC1234", transactionId: "txn:tip:ABC1234",
expirationTimestamp: { expirationTimestamp: {
t_s: 1, t_s: 1,
}, },
tipAmountRaw: "", rewardAmountRaw: "",
}); });
const props: Props = { const props: Props = {

View File

@ -22,7 +22,7 @@
export * as a1 from "./Deposit/stories.jsx"; export * as a1 from "./Deposit/stories.jsx";
export * as a3 from "./Payment/stories.jsx"; export * as a3 from "./Payment/stories.jsx";
export * as a4 from "./Refund/stories.jsx"; export * as a4 from "./Refund/stories.jsx";
export * as a5 from "./Tip/stories.jsx"; export * as a5 from "./Reward/stories.js";
export * as a6 from "./Withdraw/stories.jsx"; export * as a6 from "./Withdraw/stories.jsx";
export * as a8 from "./InvoiceCreate/stories.js"; export * as a8 from "./InvoiceCreate/stories.js";
export * as a9 from "./InvoicePay/stories.js"; export * as a9 from "./InvoicePay/stories.js";

View File

@ -279,7 +279,7 @@ function openWalletURIFromPopup(uri: TalerUri): void {
`static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`, `static/wallet.html#/cta/pay?talerUri=${encodeURIComponent(talerUri)}`,
); );
break; break;
case TalerUriAction.Tip: case TalerUriAction.Reward:
url = chrome.runtime.getURL( url = chrome.runtime.getURL(
`static/wallet.html#/cta/tip?talerUri=${encodeURIComponent(talerUri)}`, `static/wallet.html#/cta/tip?talerUri=${encodeURIComponent(talerUri)}`,
); );

View File

@ -65,7 +65,7 @@ function ContentByUriType({
</Button> </Button>
</div> </div>
); );
case TalerUriAction.Tip: case TalerUriAction.Reward:
return ( return (
<div> <div>
<p> <p>

View File

@ -66,7 +66,7 @@ export function AddNewActionView({ onCancel }: Props): VNode {
return <i18n.Translate>Open pay page</i18n.Translate>; return <i18n.Translate>Open pay page</i18n.Translate>;
case TalerUriAction.Refund: case TalerUriAction.Refund:
return <i18n.Translate>Open refund page</i18n.Translate>; return <i18n.Translate>Open refund page</i18n.Translate>;
case TalerUriAction.Tip: case TalerUriAction.Reward:
return <i18n.Translate>Open tip page</i18n.Translate>; return <i18n.Translate>Open tip page</i18n.Translate>;
case TalerUriAction.Withdraw: case TalerUriAction.Withdraw:
return <i18n.Translate>Open withdraw page</i18n.Translate>; return <i18n.Translate>Open withdraw page</i18n.Translate>;

View File

@ -58,7 +58,7 @@ import { PaymentPage } from "../cta/Payment/index.js";
import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js"; import { PaymentTemplatePage } from "../cta/PaymentTemplate/index.js";
import { RecoveryPage } from "../cta/Recovery/index.js"; import { RecoveryPage } from "../cta/Recovery/index.js";
import { RefundPage } from "../cta/Refund/index.js"; import { RefundPage } from "../cta/Refund/index.js";
import { TipPage } from "../cta/Tip/index.js"; import { TipPage } from "../cta/Reward/index.js";
import { TransferCreatePage } from "../cta/TransferCreate/index.js"; import { TransferCreatePage } from "../cta/TransferCreate/index.js";
import { TransferPickupPage } from "../cta/TransferPickup/index.js"; import { TransferPickupPage } from "../cta/TransferPickup/index.js";
import { import {

View File

@ -34,7 +34,7 @@ import {
TransactionPeerPushDebit, TransactionPeerPushDebit,
TransactionRefresh, TransactionRefresh,
TransactionRefund, TransactionRefund,
TransactionTip, TransactionReward,
TransactionType, TransactionType,
TransactionWithdrawal, TransactionWithdrawal,
WithdrawalType, WithdrawalType,
@ -113,9 +113,9 @@ const exampleData = {
} as TransactionRefresh, } as TransactionRefresh,
tip: { tip: {
...commonTransaction(), ...commonTransaction(),
type: TransactionType.Tip, type: TransactionType.Reward,
merchantBaseUrl: "http://ads.merchant.taler.net/", merchantBaseUrl: "http://ads.merchant.taler.net/",
} as TransactionTip, } as TransactionReward,
refund: { refund: {
...commonTransaction(), ...commonTransaction(),
type: TransactionType.Refund, type: TransactionType.Refund,

View File

@ -37,7 +37,7 @@ import {
TransactionPeerPushDebit, TransactionPeerPushDebit,
TransactionRefresh, TransactionRefresh,
TransactionRefund, TransactionRefund,
TransactionTip, TransactionReward,
TransactionType, TransactionType,
TransactionWithdrawal, TransactionWithdrawal,
WithdrawalDetails, WithdrawalDetails,
@ -138,7 +138,7 @@ const exampleData = {
} as TransactionRefresh, } as TransactionRefresh,
tip: { tip: {
...commonTransaction, ...commonTransaction,
type: TransactionType.Tip, type: TransactionType.Reward,
// merchant: { // merchant: {
// name: "the merchant", // name: "the merchant",
// logo: merchantIcon, // logo: merchantIcon,
@ -146,7 +146,7 @@ const exampleData = {
// email: "contact@merchant.taler", // email: "contact@merchant.taler",
// }, // },
merchantBaseUrl: "http://merchant.taler", merchantBaseUrl: "http://merchant.taler",
} as TransactionTip, } as TransactionReward,
refund: { refund: {
...commonTransaction, ...commonTransaction,
type: TransactionType.Refund, type: TransactionType.Refund,

View File

@ -745,7 +745,7 @@ export function TransactionView({
); );
} }
if (transaction.type === TransactionType.Tip) { if (transaction.type === TransactionType.Reward) {
return ( return (
<TransactionTemplate <TransactionTemplate
transaction={transaction} transaction={transaction}

View File

@ -2,8 +2,7 @@
function setupLiveReload(): void { function setupLiveReload(): void {
const protocol = window.location.protocol === "http:" ? "ws:" : "wss:"; const protocol = window.location.protocol === "http:" ? "ws:" : "wss:";
const port = window.location.protocol === "http:" ? "8080" : "8081"; const ws = new WebSocket(`${protocol}//${window.location.hostname}:${window.location.port}/ws`);
const ws = new WebSocket(`${protocol}//localhost:${port}/ws`);
ws.addEventListener("message", (message) => { ws.addEventListener("message", (message) => {
try { try {

View File

@ -1,4 +1,4 @@
lockfileVersion: '6.0' lockfileVersion: '6.1'
settings: settings:
autoInstallPeers: true autoInstallPeers: true