implement payto URIs

This commit is contained in:
Florian Dold 2019-05-08 04:53:26 +02:00
parent 3db38fbd0b
commit 6904c2759e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 82 additions and 118 deletions

View File

@ -34,7 +34,6 @@ import {
MerchantRefundPermission,
PayReq,
TipResponse,
WireDetail,
} from "./talerTypes";
import {
@ -114,10 +113,10 @@ export interface ReserveRecord {
hasPayback: boolean;
/**
* Wire information for the bank account that
* Wire information (as payto URI) for the bank account that
* transfered funds for this reserve.
*/
senderWire?: object;
senderWire?: string;
}
@ -837,15 +836,7 @@ export interface PurchaseRecord {
* Information about wire information for bank accounts we withdrew coins from.
*/
export interface SenderWireRecord {
/**
* Wire details.
*/
senderWire: WireDetail;
/**
* Identifier, hash code of canonicalized senderWire.
*/
id: number;
paytoUri: string;
}
@ -1001,7 +992,7 @@ export namespace Stores {
class SenderWiresStore extends Store<SenderWireRecord> {
constructor() {
super("senderWires", { keyPath: "id" });
super("senderWires", { keyPath: "paytoUri" });
}
}

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -239,7 +239,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -235,7 +235,7 @@ msgstr ""
#. #-#-#-#-# - (PACKAGE VERSION) #-#-#-#-#
#. TODO:generic error reporting function or component.
#: src/webex/pages/confirm-create-reserve.tsx:511 src/webex/pages/tip.tsx:180
#: src/webex/pages/confirm-create-reserve.tsx:515 src/webex/pages/tip.tsx:180
#, c-format
msgid "Fatal error: \"%1$s\"."
msgstr ""

View File

@ -852,29 +852,25 @@ export class WireFeesJson {
}
/**
* Information about wire transfer methods supported
* by the exchange.
*/
@Checkable.Class({extra: true})
export class WireDetailJson {
/**
* Name of the wire transfer method.
*/
@Checkable.Class()
export class AccountInfo {
@Checkable.String()
type: string;
url: string;
/**
* Fees associated with the wire transfer method.
*/
@Checkable.List(Checkable.Value(() => WireFeesJson))
fees: WireFeesJson[];
@Checkable.String()
master_sig: string;
}
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => WireDetailJson;
@Checkable.Class({extra: true})
export class ExchangeWireJson {
@Checkable.Map(Checkable.String(), Checkable.List(Checkable.Value(() => WireFeesJson)))
fees: { [methodName: string]: WireFeesJson[] };
@Checkable.List(Checkable.Value(() => AccountInfo))
accounts: AccountInfo[];
static checked: (obj: any) => ExchangeWireJson;
}
@ -884,12 +880,6 @@ export class WireDetailJson {
*/
export type WireDetail = object & { type: string };
/**
* Type guard for wire details.
*/
export function isWireDetail(x: any): x is WireDetail {
return x && typeof x === "object" && typeof x.type === "string";
}
/**
* Proposal returned from the contract URL.

View File

@ -28,7 +28,6 @@ import {
canonicalJson,
canonicalizeBaseUrl,
getTalerStampSec,
hash,
strcmp,
} from "./helpers";
import {
@ -75,6 +74,7 @@ import {
ContractTerms,
Denomination,
ExchangeHandle,
ExchangeWireJson,
KeysJson,
MerchantRefundPermission,
MerchantRefundResponse,
@ -86,8 +86,6 @@ import {
TipPlanchetDetail,
TipResponse,
TipToken,
WireDetailJson,
isWireDetail,
} from "./talerTypes";
import {
Badge,
@ -109,7 +107,6 @@ import {
TipStatus,
WalletBalance,
WalletBalanceEntry,
WireInfo,
} from "./walletTypes";
@ -1133,10 +1130,9 @@ export class Wallet {
};
const senderWire = req.senderWire;
if (isWireDetail(senderWire)) {
if (senderWire) {
const rec = {
id: hash(senderWire),
senderWire,
paytoUri: senderWire,
};
await this.q().put(Stores.senderWires, rec).finish();
}
@ -1327,7 +1323,7 @@ export class Wallet {
/**
* Get the wire information for the exchange with the given base URL.
*/
async getWireInfo(exchangeBaseUrl: string): Promise<WireInfo> {
async getWireInfo(exchangeBaseUrl: string): Promise<ExchangeWireJson> {
exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl);
const reqUrl = new URI("wire").absoluteTo(exchangeBaseUrl);
const resp = await this.http.get(reqUrl.href());
@ -1340,7 +1336,7 @@ export class Wallet {
if (!wiJson) {
throw Error("/wire response malformed");
}
return wiJson;
return ExchangeWireJson.checked(wiJson)
}
@ -1500,6 +1496,11 @@ export class Wallet {
throw Error(`no wire fees found for exchange ${baseUrl}`);
}
const exchangeWireAccounts: string[] = [];
for (let account of wireInfo.accounts) {
exchangeWireAccounts.push(account.url);
}
const {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo);
let earliestDepositExpiration = Infinity;
@ -1534,10 +1535,10 @@ export class Wallet {
}
}
const ret: ReserveCreationInfo = {
earliestDepositExpiration,
exchangeInfo,
exchangeWireAccounts,
exchangeVersion: exchangeInfo.protocolVersion || "unknown",
isAudited,
isTrusted,
@ -1548,7 +1549,6 @@ export class Wallet {
versionMatch,
walletVersion: WALLET_PROTOCOL_VERSION,
wireFees,
wireInfo,
withdrawFee: acc,
};
return ret;
@ -1563,26 +1563,13 @@ export class Wallet {
async updateExchangeFromUrl(baseUrl: string): Promise<ExchangeRecord> {
baseUrl = canonicalizeBaseUrl(baseUrl);
const keysUrl = new URI("keys").absoluteTo(baseUrl);
const wireUrl = new URI("wire").absoluteTo(baseUrl);
const keysResp = await this.http.get(keysUrl.href());
if (keysResp.status !== 200) {
throw Error("/keys request failed");
}
const wireResp = await this.http.get(wireUrl.href());
if (wireResp.status !== 200) {
throw Error("/wire request failed");
}
const exchangeKeysJson = KeysJson.checked(JSON.parse(keysResp.responseText));
const wireRespJson = JSON.parse(wireResp.responseText);
if (typeof wireRespJson !== "object") {
throw Error("/wire response is not an object");
}
console.log("exchange wire", wireRespJson);
const wireMethodDetails: WireDetailJson[] = [];
for (const methodName in wireRespJson) {
wireMethodDetails.push(WireDetailJson.checked(wireRespJson[methodName]));
}
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, wireMethodDetails);
const exchangeWire = await this.getWireInfo(baseUrl);
return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire);
}
@ -1614,7 +1601,7 @@ export class Wallet {
private async updateExchangeFromJson(baseUrl: string,
exchangeKeysJson: KeysJson,
wireMethodDetails: WireDetailJson[]): Promise<ExchangeRecord> {
wireMethodDetails: ExchangeWireJson): Promise<ExchangeRecord> {
// FIXME: all this should probably be commited atomically
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
@ -1667,16 +1654,17 @@ export class Wallet {
};
}
for (const detail of wireMethodDetails) {
for (const paytoTargetType in wireMethodDetails.fees) {
let latestFeeStamp = 0;
const fees = oldWireFees.feesForType[detail.type] || [];
oldWireFees.feesForType[detail.type] = fees;
for (const oldFee of fees) {
const newFeeDetails = wireMethodDetails.fees[paytoTargetType];
const oldFeeDetails = oldWireFees.feesForType[paytoTargetType] || [];
oldWireFees.feesForType[paytoTargetType] = oldFeeDetails;
for (const oldFee of oldFeeDetails) {
if (oldFee.endStamp > latestFeeStamp) {
latestFeeStamp = oldFee.endStamp;
}
}
for (const fee of detail.fees) {
for (const fee of newFeeDetails) {
const start = getTalerStampSec(fee.start_date);
if (start === null) {
console.error("invalid start stamp in fee", fee);
@ -1697,12 +1685,12 @@ export class Wallet {
startStamp: start,
wireFee: Amounts.parseOrThrow(fee.wire_fee),
};
const valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey);
const valid: boolean = await this.cryptoApi.isValidWireFee(paytoTargetType, wf, exchangeInfo.masterPublicKey);
if (!valid) {
console.error("fee signature invalid", fee);
throw Error("fee signature invalid");
}
fees.push(wf);
oldFeeDetails.push(wf);
}
}
@ -2434,11 +2422,9 @@ export class Wallet {
const senderWiresSet = new Set();
await this.q().iter(Stores.senderWires).map((x) => {
if (x.senderWire) {
senderWiresSet.add(canonicalJson(x.senderWire));
}
senderWiresSet.add(x.paytoUri);
}).run();
const senderWires = Array.from(senderWiresSet).map((x) => JSON.parse(x));
const senderWires = Array.from(senderWiresSet);
return {
exchangeWireTypes,

View File

@ -70,19 +70,6 @@ export class CreateReserveResponse {
}
/**
* Wire info, sent to the bank when creating a reserve. Fee information will
* be filtered out. Only methods that the bank also supports should be sent.
*/
export interface WireInfo {
/**
* Mapping from wire method type to the exchange's wire info,
* excluding fees.
*/
[type: string]: any;
}
/**
* Information about what will happen when creating a reserve.
*
@ -97,7 +84,7 @@ export interface ReserveCreationInfo {
/**
* Filtered wire info to send to the bank.
*/
wireInfo: WireInfo;
exchangeWireAccounts: string[];
/**
* Selected denominations for withdraw.
@ -139,6 +126,7 @@ export interface ReserveCreationInfo {
* Number of currently offered denominations.
*/
numOfferedDenoms: number;
/**
* Public keys of trusted auditors for the currency we're withdrawing.
*/
@ -337,10 +325,11 @@ export class CreateReserveRequest {
exchange: string;
/**
* Wire details for the bank account that sent the funds to the exchange.
* Wire details (as a payto URI) for the bank account that sent the funds to
* the exchange.
*/
@Checkable.Optional(Checkable.Any())
senderWire?: object;
@Checkable.Optional(Checkable.String())
senderWire?: string;
/**
* Verify that a value matches the schema of this class and convert it into a

View File

@ -92,7 +92,7 @@ interface ExchangeSelectionProps {
callback_url: string;
wt_types: string[];
currencyRecord: CurrencyRecord|null;
sender_wire: object | undefined;
sender_wire: string | undefined;
}
interface ManualSelectionProps {
@ -410,7 +410,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
exchange: string,
amount: AmountJson,
callback_url: string,
sender_wire: object | undefined) {
sender_wire: string | undefined) {
const rawResp = await createReserve({
amount,
exchange: canonicalizeBaseUrl(exchange),
@ -420,21 +420,25 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
throw Error("empty response");
}
// FIXME: filter out types that bank/exchange don't have in common
const wireDetails = rci.wireInfo;
const filteredWireDetails: any = {};
for (const wireType in wireDetails) {
if (this.props.wt_types.findIndex((x) => x.toLowerCase() === wireType.toLowerCase()) < 0) {
const exchangeWireAccounts = [];
for (let acct of rci.exchangeWireAccounts) {
const payto = new URI(acct);
if (payto.scheme() != "payto") {
console.warn("unknown wire account URI scheme", acct);
continue;
}
const obj = Object.assign({}, wireDetails[wireType]);
// The bank doesn't need to know about fees
delete obj.fees;
// Consequently the bank can't verify signatures anyway, so
// we delete this extra data, to make the request URL shorter.
delete obj.salt;
delete obj.sig;
filteredWireDetails[wireType] = obj;
if (this.props.wt_types.includes(payto.authority())) {
exchangeWireAccounts.push(acct);
}
}
const chosenAcct = exchangeWireAccounts[0];
if (!chosenAcct) {
throw Error("no exchange account matches the bank's supported types");
}
if (!rawResp.error) {
const resp = CreateReserveResponse.checked(rawResp);
const q: {[name: string]: string|number} = {
@ -442,7 +446,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
amount_fraction: amount.fraction,
amount_value: amount.value,
exchange: resp.exchange,
exchange_wire_details: JSON.stringify(filteredWireDetails),
exchange_wire_details: chosenAcct,
reserve_pub: resp.reservePub,
};
const url = new URI(callback_url).addQuery(q);
@ -487,7 +491,11 @@ async function main() {
let sender_wire;
if (query.sender_wire) {
sender_wire = JSON.parse(query.sender_wire);
let senderWireUri = new URI(query.sender_wire);
if (senderWireUri.scheme() != "payto") {
throw Error("sender wire info must be a payto URI");
}
sender_wire = query.sender_wire;
}
const suggestedExchangeUrl = query.suggested_exchange_url;

View File

@ -273,7 +273,7 @@ export function checkUpgrade(): Promise<UpgradeResponse> {
/**
* Create a reserve.
*/
export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: object }): Promise<any> {
export function createReserve(args: { amount: AmountJson, exchange: string, senderWire?: string }): Promise<any> {
return callBackend("create-reserve", args);
}

View File

@ -18,7 +18,7 @@ module.exports = function (env) {
const base = {
output: {
filename: '[name]-bundle.js',
chunkFilename: "[id]-bundle.js",
chunkFilename: "[name]-bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {