ledgerhq / xpub-scan Goto Github PK
View Code? Open in Web Editor NEWTool to perform master public key analysis
License: Other
Tool to perform master public key analysis
License: Other
Some wallets use different derivation paths.
xpub-scan should support paths such as:
m/0'/0'
Bitcoin Core harden addressesm/0'/0
Multibit/BRD/breadwalletIt would be nice if this tool could support those hardened paths also.
I imagine this might take considerable effort as you'd essentially be importing a xpriv key?
Currently, the requests are synchronous and the underlying implementation does not properly handle exceptions (no retry, a network error harshly throws an un-recoverable error, no JSON parsing exceptions handling, etc.).
Lines 13 to 35 in 3b64b7f
This basic implementation could be greatly improved by:
From the user perspective, the new implementation should:
When performing a comparison between imported operations and actuals ones, all categories of comparison are shown: matches, mismatches, etc.
It can be tedious to find the relevant information in these conditions, especially with thousands of operations.
An improvement of the current implementation could be the creation of an option (e.g., --show-diffs-only
) that would only display the discrepancies: mismatches, missing operations, and extra operations.
When I run xpub-scan --currency bch <xpub address>
(actual address removed for privacy), I get:
(Data fetched from the default provider)
Active addresses
Scanning Bitcoin Cash addresses...
- scanning external addresses -
Bitcoin Cash m/0/0 1FTQDiaeLfSNoUuj72ZYB1uDaRHF67pHG3 qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr analyzing...node:internal/process/promises:279
triggerUncaughtException(err, true /* fromPromise */);
^
<ref *1> Error: connect ECONNREFUSED 13.51.66.122:443
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1195:16) {
errno: -61,
code: 'ECONNREFUSED',
syscall: 'connect',
address: '13.51.66.122',
port: 443,
config: {
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
adapter: [Function: httpAdapter],
transformRequest: [ [Function: transformRequest] ],
transformResponse: [ [Function: transformResponse] ],
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
validateStatus: [Function: validateStatus],
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/0.26.1'
},
method: 'get',
url: 'https://rest.bitcoin.com/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr',
data: undefined
},
request: <ref *4> Writable {
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: false,
decodeStrings: true,
defaultEncoding: 'utf8',
length: 0,
writing: false,
corked: 0,
sync: true,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: null,
writelen: 0,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 0,
constructed: true,
prefinished: false,
errorEmitted: false,
emitClose: true,
autoDestroy: true,
errored: null,
closed: false,
closeEmitted: false,
[Symbol(kOnFinished)]: []
},
_events: [Object: null prototype] {
response: [Function: handleResponse],
error: [Function: handleRequestError],
socket: [Function: handleRequestSocket]
},
_eventsCount: 3,
_maxListeners: undefined,
_options: {
maxRedirects: 21,
maxBodyLength: 10485760,
protocol: 'https:',
path: '/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr',
method: 'GET',
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/0.26.1'
},
agent: undefined,
agents: { http: undefined, https: undefined },
auth: undefined,
hostname: 'rest.bitcoin.com',
port: null,
nativeProtocols: {
'http:': {
_connectionListener: [Function: connectionListener],
METHODS: [
'ACL', 'BIND', 'CHECKOUT',
'CONNECT', 'COPY', 'DELETE',
'GET', 'HEAD', 'LINK',
'LOCK', 'M-SEARCH', 'MERGE',
'MKACTIVITY', 'MKCALENDAR', 'MKCOL',
'MOVE', 'NOTIFY', 'OPTIONS',
'PATCH', 'POST', 'PROPFIND',
'PROPPATCH', 'PURGE', 'PUT',
'REBIND', 'REPORT', 'SEARCH',
'SOURCE', 'SUBSCRIBE', 'TRACE',
'UNBIND', 'UNLINK', 'UNLOCK',
'UNSUBSCRIBE'
],
STATUS_CODES: {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a Teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Too Early',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended',
'511': 'Network Authentication Required'
},
Agent: [Function: Agent] { defaultMaxSockets: Infinity },
ClientRequest: [Function: ClientRequest],
IncomingMessage: [Function: IncomingMessage],
OutgoingMessage: [Function: OutgoingMessage],
Server: [Function: Server],
ServerResponse: [Function: ServerResponse],
createServer: [Function: createServer],
validateHeaderName: [Function: __node_internal_],
validateHeaderValue: [Function: __node_internal_],
get: [Function: get],
request: [Function: request],
maxHeaderSize: [Getter],
globalAgent: [Getter/Setter]
},
'https:': {
Agent: [Function: Agent],
globalAgent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object: null prototype],
requests: [Object: null prototype] {},
sockets: [Object: null prototype],
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
maxCachedSessions: 100,
_sessionCache: [Object],
[Symbol(kCapture)]: false
},
Server: [Function: Server],
createServer: [Function: createServer],
get: [Function: get],
request: [Function: request]
}
},
pathname: '/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr'
},
_ended: true,
_ending: true,
_redirectCount: 0,
_redirects: [],
_requestBodyLength: 0,
_requestBodyBuffers: [],
_onNativeResponse: [Function (anonymous)],
_currentRequest: <ref *2> ClientRequest {
_events: [Object: null prototype] {
response: [Function: bound onceWrapper] {
listener: [Function (anonymous)]
},
abort: [Function (anonymous)],
aborted: [Function (anonymous)],
connect: [Function (anonymous)],
error: [Function (anonymous)],
socket: [Function (anonymous)],
timeout: [Function (anonymous)]
},
_eventsCount: 7,
_maxListeners: undefined,
outputData: [],
outputSize: 0,
writable: true,
destroyed: false,
_last: true,
chunkedEncoding: false,
shouldKeepAlive: false,
maxRequestsOnConnectionReached: false,
_defaultKeepAlive: true,
useChunkedEncodingByDefault: false,
sendDate: false,
_removedConnection: false,
_removedContLen: false,
_removedTE: false,
_contentLength: 0,
_hasBody: true,
_trailer: '',
finished: true,
_headerSent: true,
_closed: false,
socket: <ref *3> TLSSocket {
_tlsOptions: {
allowHalfOpen: undefined,
pipe: false,
secureContext: SecureContext { context: SecureContext {} },
isServer: false,
requestCert: true,
rejectUnauthorized: true,
session: undefined,
ALPNProtocols: undefined,
requestOCSP: undefined,
enableTrace: undefined,
pskCallback: undefined,
highWaterMark: undefined,
onread: undefined,
signal: undefined
},
_secureEstablished: false,
_securePending: false,
_newSessionPending: false,
_controlReleased: true,
secureConnecting: true,
_SNICallback: null,
servername: null,
alpnProtocol: null,
authorized: false,
authorizationError: null,
encrypted: true,
_events: [Object: null prototype] {
close: [
[Function: onSocketCloseDestroySSL],
[Function],
[Function: onClose],
[Function: socketCloseListener]
],
end: [ [Function: onConnectEnd], [Function: onReadableStreamEnd] ],
newListener: [Function: keylogNewListener],
connect: [ [Function], [Function], [Function] ],
secure: [Function: onConnectSecure],
session: [Function (anonymous)],
free: [Function: onFree],
timeout: [Function: onTimeout],
agentRemove: [Function: onRemove],
error: [Function: socketErrorListener],
drain: [Function: ondrain]
},
_eventsCount: 11,
connecting: false,
_hadError: true,
_parent: null,
_host: 'rest.bitcoin.com',
_readableState: ReadableState {
objectMode: false,
highWaterMark: 16384,
buffer: BufferList { head: null, tail: null, length: 0 },
length: 0,
pipes: [],
flowing: true,
ended: false,
endEmitted: false,
reading: true,
constructed: true,
sync: false,
needReadable: true,
emittedReadable: false,
readableListening: false,
resumeScheduled: false,
errorEmitted: true,
emitClose: false,
autoDestroy: true,
destroyed: true,
errored: [Circular *1],
closed: true,
closeEmitted: true,
defaultEncoding: 'utf8',
awaitDrainWriters: null,
multiAwaitDrain: false,
readingMore: false,
dataEmitted: false,
decoder: null,
encoding: null,
[Symbol(kPaused)]: false
},
_maxListeners: undefined,
_writableState: WritableState {
objectMode: false,
highWaterMark: 16384,
finalCalled: false,
needDrain: false,
ending: false,
ended: false,
finished: false,
destroyed: true,
decodeStrings: false,
defaultEncoding: 'utf8',
length: 203,
writing: true,
corked: 0,
sync: false,
bufferProcessing: false,
onwrite: [Function: bound onwrite],
writecb: [Function: bound onFinish],
writelen: 203,
afterWriteTickInfo: null,
buffered: [],
bufferedIndex: 0,
allBuffers: true,
allNoop: true,
pendingcb: 1,
constructed: true,
prefinished: false,
errorEmitted: true,
emitClose: false,
autoDestroy: true,
errored: [Circular *1],
closed: true,
closeEmitted: true,
[Symbol(kOnFinished)]: []
},
allowHalfOpen: false,
_sockname: null,
_pendingData: 'GET /v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr HTTP/1.1\r\n' +
'Accept: application/json, text/plain, */*\r\n' +
'User-Agent: axios/0.26.1\r\n' +
'Host: rest.bitcoin.com\r\n' +
'Connection: close\r\n' +
'\r\n',
_pendingEncoding: 'latin1',
server: undefined,
_server: null,
ssl: null,
_requestCert: true,
_rejectUnauthorized: true,
parser: null,
_httpMessage: [Circular *2],
[Symbol(res)]: TLSWrap {
_parent: TCP {
reading: [Getter/Setter],
onconnection: null,
[Symbol(owner_symbol)]: [Circular *3],
[Symbol(handle_onclose)]: [Function: done]
},
_parentWrap: undefined,
_secureContext: SecureContext { context: SecureContext {} },
reading: false,
onkeylog: [Function: onkeylog],
onhandshakestart: {},
onhandshakedone: [Function (anonymous)],
onocspresponse: [Function: onocspresponse],
onnewsession: [Function: onnewsessionclient],
onerror: [Function: onerror],
[Symbol(owner_symbol)]: [Circular *3]
},
[Symbol(verified)]: false,
[Symbol(pendingSession)]: null,
[Symbol(async_id_symbol)]: 58,
[Symbol(kHandle)]: null,
[Symbol(lastWriteQueueSize)]: 0,
[Symbol(timeout)]: null,
[Symbol(kBuffer)]: null,
[Symbol(kBufferCb)]: null,
[Symbol(kBufferGen)]: null,
[Symbol(kCapture)]: false,
[Symbol(kSetNoDelay)]: false,
[Symbol(kSetKeepAlive)]: true,
[Symbol(kSetKeepAliveInitialDelay)]: 60,
[Symbol(kBytesRead)]: 0,
[Symbol(kBytesWritten)]: 0,
[Symbol(connect-options)]: {
rejectUnauthorized: true,
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
checkServerIdentity: [Function: checkServerIdentity],
minDHSize: 1024,
maxRedirects: 21,
maxBodyLength: 10485760,
protocol: 'https:',
path: null,
method: 'GET',
headers: {
Accept: 'application/json, text/plain, */*',
'User-Agent': 'axios/0.26.1'
},
agent: undefined,
agents: { http: undefined, https: undefined },
auth: undefined,
hostname: 'rest.bitcoin.com',
port: 443,
nativeProtocols: { 'http:': [Object], 'https:': [Object] },
pathname: '/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr',
_defaultAgent: Agent {
_events: [Object: null prototype],
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object: null prototype],
requests: [Object: null prototype] {},
sockets: [Object: null prototype],
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
maxCachedSessions: 100,
_sessionCache: [Object],
[Symbol(kCapture)]: false
},
host: 'rest.bitcoin.com',
servername: 'rest.bitcoin.com',
_agentKey: 'rest.bitcoin.com:443:::::::::::::::::::::',
encoding: null,
singleUse: true
}
},
_header: 'GET /v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr HTTP/1.1\r\n' +
'Accept: application/json, text/plain, */*\r\n' +
'User-Agent: axios/0.26.1\r\n' +
'Host: rest.bitcoin.com\r\n' +
'Connection: close\r\n' +
'\r\n',
_keepAliveTimeout: 0,
_onPendingData: [Function: nop],
agent: Agent {
_events: [Object: null prototype] {
free: [Function (anonymous)],
newListener: [Function: maybeEnableKeylog]
},
_eventsCount: 2,
_maxListeners: undefined,
defaultPort: 443,
protocol: 'https:',
options: [Object: null prototype] { path: null },
requests: [Object: null prototype] {},
sockets: [Object: null prototype] {
'rest.bitcoin.com:443:::::::::::::::::::::': [ [TLSSocket] ]
},
freeSockets: [Object: null prototype] {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256,
scheduling: 'lifo',
maxTotalSockets: Infinity,
totalSocketCount: 1,
maxCachedSessions: 100,
_sessionCache: { map: {}, list: [] },
[Symbol(kCapture)]: false
},
socketPath: undefined,
method: 'GET',
maxHeaderSize: undefined,
insecureHTTPParser: undefined,
path: '/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr',
_ended: false,
res: null,
aborted: false,
timeoutCb: null,
upgradeOrConnect: false,
parser: null,
maxHeadersCount: null,
reusedSocket: false,
host: 'rest.bitcoin.com',
protocol: 'https:',
_redirectable: [Circular *4],
[Symbol(kCapture)]: false,
[Symbol(kNeedDrain)]: false,
[Symbol(corked)]: 0,
[Symbol(kOutHeaders)]: [Object: null prototype] {
accept: [ 'Accept', 'application/json, text/plain, */*' ],
'user-agent': [ 'User-Agent', 'axios/0.26.1' ],
host: [ 'Host', 'rest.bitcoin.com' ]
}
},
_currentUrl: 'https://rest.bitcoin.com/v2/address/details/bitcoincash:qz0gla2ugy2y3l3heu4jhukj86mul7t76gewxvu2wr',
[Symbol(kCapture)]: false
},
response: undefined,
isAxiosError: true,
toJSON: [Function: toJSON]
}
Node.js v17.9.0
Is there any way to use the scan feature directly via nodejs?
When an xpub is associated with an important number of operations, it can be cumbersome to read the generated HTML report. Indeed, the Transactions
tab can contain hundreds or thousands of rows...
A CSS pagination could improve the readability of such reports.
Can we have an option to include unused addresses (those without balance) to appear on output? That would be a great help if we experience gap limit error.
Sent to self operations are associated with the following glyph: ⮂
(U+2B82).
It is not being properly rendered on Ubuntu (20.04):
This glyph is being used here:
Lines 98 to 100 in 3b64b7f
Lines 132 to 150 in 3b64b7f
The expected output is the following one:
From this screen capture, it appears that this glyph is not very well rendered on MacOS too. Therefore, it would be acceptable for the fix to suggest another glyph to evoke the idea of sending to the same address (and that would be different from the glyph related to an address sending to another one belonging to the same xpub, and rendered with ↺
).
It should be noted that both ⮂
and ↺
glyphs are rendered as ?
on Windows.
The configuration options are set in src/settings.ts
.
Therefore, each time an end-user configures the tool, it has to be rebuilt.
A more user-friendly approach could be to make it a JSON (or, preferably, a YAML) file automatically parsed (and validated) when the user runs the tool.
HI,
Below API is no longer working https://github.com/LedgerHQ/xpub-scan/blob/main/src/configuration/settings.ts
general: "https://sochain.com/api/v2/address/{currency}/{address}"
Xpub Scan is not user-friendly (yet): it is a command-line tool that has to be built by the end-user.
Here are some ideas of improvement in this context:
On this topic, see also: #14
Hi,
If I would use xpub-scan to build an accounting tool for myself and my friends, would I be putting ourselves in danger?
Xpub Scan does not check the date fields from Type A CSVs.
It could be improved to check the maximum number of Type A CSVs fields. (Or at least ascertain a certain consistency between creation and broadcasting dates).
Type A and Type B CSV files generally handle operations on a TXID basis: in most cases, one row corresponds to one TXID.
As a consequence, Xpub Scan will falsely identify some mismatches because it operates at a more granular level.
An example that speaks for itself: in this screenshot, the imported CSV shows only one operation (on the left) while Xpub Scan identifies two distinct operations (on the right):
The first comparison is labeled as a mismatch, because the amounts differ, and the second one is labeled as a missing operation, because of a missing expected imported operation.
It quickly appears that the imported operation is the aggregation of the two operations identified by Xpub Scan: the imported amount is the sum of the actual amounts (0.00000547 × 2 = 0.00001094
). In other words, the result of this comparison should not be labeled as a mismatch.
Therefore, the current implementation could be improved by having the ability to perform aggregated comparisons when required. A specific color could be used in the HTML report to highlight such a match (or even mismatch), allowing to spot at first glance that an aggregated-type comparison has been performed for a given transaction.
Currently, Xpub Scan can automatically import and analyse type A and type B CSVs.
Type B JSONs can be conveniently generated via a command instead of manually. Making Xpub Scan compatible with this format would then facilitate the analysis of transactions at a CI/CD level.
The tool only handles two specific external providers: a default (free) one, and a custom (paid) one.
The implementation could be improved by generalizing the support for external providers. Notably, a more robust way of transforming the raw responses into raw transaction and transaction models could be suggested.
Here are some possible checks:
Refactor the code to have some kind of dispatcher to select the relevant provider.
Note that the requests underlying implementation should be improved beforehand.
In this context, see also: #7 (review)
The latest minor version of Axios has made a change requiring 'Accept-Encoding': 'application/json'
to be sent in the header of a axios reqest with json response, otherwise the response is broken. When broken, it fails to scan addresses for balances. NPM auto updates to this version unfortunately.
Requires adding the addtional header to here: https://github.com/LedgerHQ/xpub-scan/blob/main/src/helpers.ts#L25
The current implementation is limited to Bitcoin mainnet and Litecoin mainnet.
The master public key is never sent over the Internet, but its derived addresses are—sequentially.
A solution could strengthen privacy in this regard (not necessarily by avoiding completely any communication to a third-party, at least at this stage).
It seems some API call restrictions or it can't handle the back to back api calls?
probing address gap...node:internal/process/promises:289
triggerUncaughtException(err, true /* fromPromise */);
^
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "AxiosError: Request failed with status code 429".] {
code: 'ERR_UNHANDLED_REJECTION'
}
Node.js v19.1.0
The workaround would be to await the response, something like this
try {
const result = await axios.post(YOUR_URL
, {});
} catch (error) {
console.error(error);
}
The current implementation does not have any unit test:
Line 8 in 3b64b7f
New unit tests could notably ensure that:
--import
feature (automatic validation of imported CSV files) correctly identifies optimal (i.e. no discrepancy) and erroneous (missing transactions, incorrect amount, etc.) CSV filesCurrently, when several addresses belonging to the xpub have sent funds in the context of a given transaction, only one is processed and displayed.
An improvement would be to handle multiple addresses taking part in a Send transaction:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.