implement payto URIs
This commit is contained in:
parent
3db38fbd0b
commit
6904c2759e
@ -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" });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user