Skip to content

Instantly share code, notes, and snippets.

@tomquisel
Created February 15, 2019 21:54
Show Gist options
  • Save tomquisel/a5b24f33df7b2b39a95a21a97ac94eaa to your computer and use it in GitHub Desktop.
Save tomquisel/a5b24f33df7b2b39a95a21a97ac94eaa to your computer and use it in GitHub Desktop.
convert Stellar XDR to minimal JSON object
// adapted from: https://github.com/stellar/laboratory/blob/master/src/utilities/extrapolateFromXdr.js
import * as _ from "lodash";
import { xdr, StrKey, Keypair, Operation } from "stellar-sdk";
/**
* Convert XDR string to minimal JSON object
* @param input input xdr string
* @param type type of XDR (TransactionResult, TransactionMeta, etc...)
*/
export function extrapolateFromXdr(input: string, type: string) {
// TODO: Check to see if type exists
// TODO: input validation
let xdrObject;
try {
// @ts-ignore
xdrObject = xdr[type].fromXDR(input, "base64");
} catch (error) {
throw new Error("Input XDR could not be parsed");
}
return buildTreeFromObject(xdrObject, type);
}
function buildTreeFromObject(object: any, name: string) {
if (_.isArray(object)) {
return parseArray(object);
} else if (!hasChildren(object)) {
if (!name) {
throw Error(`Values must know their names: ${JSON.stringify(object)}`);
}
return getValue(object, name);
} else if (object.switch) {
return parseArm(object);
} else {
return parseNormal(object);
}
}
function parseArray(array: any[]): any[] {
return array.map((item, index) =>
buildTreeFromObject(item, index.toString()),
);
}
function parseArm(object: any): object {
const name = object.switch().name;
if (_.isString(object.arm())) {
return {
[name]: buildTreeFromObject(object[object.arm()](), name),
};
}
return name;
}
function parseNormal(object: any) {
const res: any = {};
_(object)
.functionsIn()
.without("toXDR")
.value()
.forEach((name) => {
res[name] = buildTreeFromObject(object[name](), name);
});
return res;
}
function hasChildren(object: any) {
// string
if (_.isString(object)) {
return false;
}
// node buffer
if (object && (object._isBuffer || Buffer.isBuffer(object))) {
return false;
}
const functions = _.functionsIn(object);
if (functions.length === 0) {
return false;
}
// int64
if (
_.includes(functions, "getLowBits") &&
_.includes(functions, "getHighBits")
) {
return false;
}
return true;
}
const amountFields = [
"amount",
"startingBalance",
"sendMax",
"destAmount",
"limit",
];
function getValue(object: any, name: string) {
if (_.includes(amountFields, name)) {
// @ts-ignore
return Operation._fromXDRAmount(object);
}
if (name === "hint") {
// strkey encoding is using base32 encoding. Encoded public key consists of:
//
// * 1 byte version byte (0x30 encoded as `G`)
// * 32 bytes public key
// * 2 bytes checksum
//
// Because base32 symbols are 5-bit, more than one symbol is needed to represent a single byte.
// Signature Hint is the last 4 bytes of the public key. So we need to try to show as many 5-bit
// chunks as possible included between bytes 30 and 33 (included).
//
// byte 1: ##### ###
// byte 2: ## ##### #
// byte 3: #### ####
// byte 4: # ##### ##
// byte 5: ### ##### <---------- 40 bits / full alignment
// byte 6: ##### ###
// byte 7: ## ##### #
//
// .....
//
// byte 26: ##### ###
// byte 27: ## ##### #
// byte 28: #### #### full b32 symbols
// byte 29: # ##### ## |--------------------------|
// byte 30: ### 48### |
// byte 31: Signature Hint start | 49### 50# | Signature Hint end
// byte 32: ## 51### 5 | |
// byte 33: 2### 53##
// byte 34: # 54### 55
// byte 35: ### 56###
//
const hintBytes = new Buffer(object, "base64");
const partialPublicKey = Buffer.concat([new Buffer(28).fill(0), hintBytes]);
const keypair = new Keypair({
type: "ed25519",
// @ts-ignore
publicKey: partialPublicKey,
});
const partialPublicKeyString =
"G" +
new Buffer(46).fill("_").toString() +
keypair.publicKey().substr(47, 5) +
new Buffer(4).fill("_").toString();
return partialPublicKeyString;
}
if (name === "ed25519" || name === "publicKeyTypeEd25519") {
const address = StrKey.encodeEd25519PublicKey(object);
return address;
}
if (name === "assetCode" || name === "assetCode4" || name === "assetCode12") {
return (object.toString() as string).replace(/\0/g, "");
}
if (object && (object._isBuffer || Buffer.isBuffer(object))) {
return new Buffer(object).toString("base64");
}
if (typeof object === "undefined") {
return;
}
// getValue is a leaf in the recursive xdr extrapolating function meaning that
// whatever this function returns will be in the final result as-is.
// Therefore, we want them in string format so that it displayable in React.
// One example of why we need this is that UnsignedHyper values won't get
// displayed unless we convert it to a string.
if (typeof object.toString === "function") {
return object.toString();
}
throw new Error(
"Internal laboratory bug: Encountered value type in XDR viewer that does not have a toString method",
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment